From 7ec127af13bb2f1e41ca8d7cd8b30c28baa8e7a4 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 26 Sep 2021 06:49:12 +0100 Subject: [PATCH 01/36] Grafted prototype arcsinh axis-scaling from stand-alone script --- lib/matplotlib/scale.py | 70 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 306a60aa4e66..479f2a81aeba 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -458,6 +458,75 @@ def get_transform(self): return self._transform + +class AsinhScale(ScaleBase): + name = 'asinh' + + def __init__(self, axis, *, a0=1.0, **kwargs): + super().__init__(axis) + self.a0 = a0 + + def get_transform(self): + return self.AsinhTransform(self.a0) + + def set_default_locators_and_formatters(self, axis): + axis.set(major_locator=AsinhScale.AsinhLocator(self.a0), major_formatter='{x:.3g}') + + class AsinhTransform(Transform): + input_dims = output_dims =1 + + def __init__(self, a0): + matplotlib.transforms.Transform.__init__(self) + self.a0 = a0 + + def transform_non_affine(self, a): + return self.a0 * np.arcsinh(a / self.a0) + + def inverted(self): + return AsinhScale.InvertedAsinhTransform(self.a0) + + class InvertedAsinhTransform(Transform): + input_dims = output_dims =1 + + def __init__(self, a0): + matplotlib.transforms.Transform.__init__(self) + self.a0 = a0 + + def transform_non_affine(self, a): + return self.a0 * np.sinh(a / self.a0) + + def inverted(self): + return AsinhScale.AsinhTransform(self.a0) + + class AsinhLocator(matplotlib.ticker.Locator): + def __init__(self, a0): + super().__init__() + self.a0 = a0 + + def __call__(self): + dmin, dmax = self.axis.get_data_interval() + return self.tick_values(dmin, dmax) + + def tick_values(self, vmin, vmax): + + ymin, ymax = self.a0 * np.arcsinh(np.array([vmin, vmax]) / self.a0) + ys = np.linspace(ymin, ymax, 12) + if (ymin * ymax) < 0: + ys = np.hstack([ ys, 0.0 ]) + + xs = self.a0 * np.sinh(ys / self.a0) + + decades = ( + np.where(xs >= 0, 1, -1) * + np.power(10, np.where(xs == 0, 1.0, + np.floor(np.log10(np.abs(xs))))) + ) + qs = decades * np.round(xs / decades) + + return np.array(sorted(set(qs))) + + + class LogitTransform(Transform): input_dims = output_dims = 1 @@ -568,6 +637,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos): 'linear': LinearScale, 'log': LogScale, 'symlog': SymmetricalLogScale, + 'asinh': AsinhScale, 'logit': LogitScale, 'function': FuncScale, 'functionlog': FuncScaleLog, From 15ff4ec101b2263e3767b5578ea9ed170c365b9f Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 26 Sep 2021 07:22:00 +0100 Subject: [PATCH 02/36] Tidied various baseclass references --- lib/matplotlib/scale.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 479f2a81aeba..d421b7ac1651 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -22,7 +22,7 @@ from matplotlib import _api, docstring from matplotlib.ticker import ( NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter, - NullLocator, LogLocator, AutoLocator, AutoMinorLocator, + Locator, NullLocator, LogLocator, AutoLocator, AutoMinorLocator, SymmetricalLogLocator, LogitLocator) from matplotlib.transforms import Transform, IdentityTransform @@ -460,6 +460,21 @@ def get_transform(self): class AsinhScale(ScaleBase): + """ + A quasi-logarithmic scale based on the inverse hyperbolic sin (asinh) + + For values close to zero, this is essentially a linear scale, + but for larger values (either positive or negative) is asymptotically + logarithmic. The transition between these linear and logarithmic regimes + is smooth, and has no discontinutities in the function gradient + in contrast to the "symlog" scale. + + Parameters + ---------- + a0 : float, default: 1 + The scale parameter defining the extent of the quasi-linear region. + """ + name = 'asinh' def __init__(self, axis, *, a0=1.0, **kwargs): @@ -476,7 +491,7 @@ class AsinhTransform(Transform): input_dims = output_dims =1 def __init__(self, a0): - matplotlib.transforms.Transform.__init__(self) + super().__init__() self.a0 = a0 def transform_non_affine(self, a): @@ -489,7 +504,7 @@ class InvertedAsinhTransform(Transform): input_dims = output_dims =1 def __init__(self, a0): - matplotlib.transforms.Transform.__init__(self) + super().__init__() self.a0 = a0 def transform_non_affine(self, a): @@ -498,7 +513,7 @@ def transform_non_affine(self, a): def inverted(self): return AsinhScale.AsinhTransform(self.a0) - class AsinhLocator(matplotlib.ticker.Locator): + class AsinhLocator(Locator): def __init__(self, a0): super().__init__() self.a0 = a0 @@ -515,11 +530,12 @@ def tick_values(self, vmin, vmax): ys = np.hstack([ ys, 0.0 ]) xs = self.a0 * np.sinh(ys / self.a0) + zero_xs = (xs == 0) decades = ( np.where(xs >= 0, 1, -1) * - np.power(10, np.where(xs == 0, 1.0, - np.floor(np.log10(np.abs(xs))))) + np.power(10, np.where(zero_xs, 1.0, + np.floor(np.log10(np.abs(xs) + zero_xs*1e-6)))) ) qs = decades * np.round(xs / decades) From 9680432331d8bccc43f235097fc76cef129f350d Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 26 Sep 2021 07:55:30 +0100 Subject: [PATCH 03/36] Added more documentation and apx_tick_count parameter --- lib/matplotlib/scale.py | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index d421b7ac1651..acabf3b77530 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -461,23 +461,24 @@ def get_transform(self): class AsinhScale(ScaleBase): """ - A quasi-logarithmic scale based on the inverse hyperbolic sin (asinh) + A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh) For values close to zero, this is essentially a linear scale, - but for larger values (either positive or negative) is asymptotically + but for larger values (either positive or negative) it is asymptotically logarithmic. The transition between these linear and logarithmic regimes is smooth, and has no discontinutities in the function gradient in contrast to the "symlog" scale. - - Parameters - ---------- - a0 : float, default: 1 - The scale parameter defining the extent of the quasi-linear region. """ name = 'asinh' def __init__(self, axis, *, a0=1.0, **kwargs): + """ + Parameters + ---------- + a0 : float, default: 1 + The scale parameter defining the extent of the quasi-linear region. + """ super().__init__(axis) self.a0 = a0 @@ -485,7 +486,8 @@ def get_transform(self): return self.AsinhTransform(self.a0) def set_default_locators_and_formatters(self, axis): - axis.set(major_locator=AsinhScale.AsinhLocator(self.a0), major_formatter='{x:.3g}') + axis.set(major_locator=AsinhScale.AsinhLocator(self.a0), + major_formatter='{x:.3g}') class AsinhTransform(Transform): input_dims = output_dims =1 @@ -514,24 +516,41 @@ def inverted(self): return AsinhScale.AsinhTransform(self.a0) class AsinhLocator(Locator): - def __init__(self, a0): + """ + An axis tick locator specialized for the arcsinh scale + + This is very unlikely to have any use beyond the AsinhScale class. + """ + def __init__(self, a0, apx_tick_count=12): + """ + Parameters + ---------- + a0 : float + The scale parameter defining the extent of the quasi-linear region. + apx_tick_count : int, default: 12 + The approximate number of major ticks that will fit along the entire axis + """ super().__init__() self.a0 = a0 + self.apx_tick_count = apx_tick_count def __call__(self): dmin, dmax = self.axis.get_data_interval() return self.tick_values(dmin, dmax) def tick_values(self, vmin, vmax): - + # Construct a set of "on-screen" locations that are uniformly spaced: ymin, ymax = self.a0 * np.arcsinh(np.array([vmin, vmax]) / self.a0) - ys = np.linspace(ymin, ymax, 12) + ys = np.linspace(ymin, ymax, self.apx_tick_count) if (ymin * ymax) < 0: + # Ensure that zero tick-mark is included if the axis stradles zero ys = np.hstack([ ys, 0.0 ]) + # Transform the "on-screen" grid to the data space: xs = self.a0 * np.sinh(ys / self.a0) zero_xs = (xs == 0) + # Round the data-space values to be intuitive decimal numbers: decades = ( np.where(xs >= 0, 1, -1) * np.power(10, np.where(zero_xs, 1.0, From 3f6978841a70aed66b0b239cf1682b1bd020260e Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 26 Sep 2021 08:15:48 +0100 Subject: [PATCH 04/36] Added demo script for asinh axis scaling --- doc/users/next_whats_new/asinh_scale.rst | 9 ++++++ examples/scales/asinh_demo.py | 40 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 doc/users/next_whats_new/asinh_scale.rst create mode 100644 examples/scales/asinh_demo.py diff --git a/doc/users/next_whats_new/asinh_scale.rst b/doc/users/next_whats_new/asinh_scale.rst new file mode 100644 index 000000000000..1756111ba59b --- /dev/null +++ b/doc/users/next_whats_new/asinh_scale.rst @@ -0,0 +1,9 @@ +New axis scale ``asinh`` +------------------------ + +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 than span many orders +of magnitude. A scale parameter ``a0`` is provided to allow the user +to tune the width of the linear region of the scale. diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py new file mode 100644 index 000000000000..19ffa373e3a7 --- /dev/null +++ b/examples/scales/asinh_demo.py @@ -0,0 +1,40 @@ +""" +============ +Asinh Demo +============ +""" + +import numpy +import matplotlib.pyplot as plt + +# Prepare sample values for variations on y=x graph: +x = numpy.linspace(-3, 6, 100) + +# Compare "symlog" and "asinh" behaviour on sample y=x graph: +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(r'$sinh^{-1}$') + + +# Compare "asinh" graphs with different scale parameter "a0": +fig2 = plt.figure() +axs = fig2.subplots(1, 3, sharex=True) +for ax, a0 in zip(axs, (0.2, 1.0, 5.0)): + ax.set_title('a0={:.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', a0=a0) + ax.grid() + ax.legend(loc='best', fontsize='small') + +plt.show() From dd4f4d89af89cf1a8c35cbac63bd15e34682fa8c Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 26 Sep 2021 09:43:40 +0100 Subject: [PATCH 05/36] Tidied various flake8 transgressions --- lib/matplotlib/scale.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index acabf3b77530..6817aa3d1b5c 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -458,7 +458,6 @@ def get_transform(self): return self._transform - class AsinhScale(ScaleBase): """ A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh) @@ -480,6 +479,8 @@ def __init__(self, axis, *, a0=1.0, **kwargs): The scale parameter defining the extent of the quasi-linear region. """ super().__init__(axis) + if a0 <= 0.0: + raise ValueError("Scale parameter 'a0' must be strictly positive") self.a0 = a0 def get_transform(self): @@ -490,7 +491,7 @@ def set_default_locators_and_formatters(self, axis): major_formatter='{x:.3g}') class AsinhTransform(Transform): - input_dims = output_dims =1 + input_dims = output_dims = 1 def __init__(self, a0): super().__init__() @@ -503,7 +504,7 @@ def inverted(self): return AsinhScale.InvertedAsinhTransform(self.a0) class InvertedAsinhTransform(Transform): - input_dims = output_dims =1 + input_dims = output_dims = 1 def __init__(self, a0): super().__init__() @@ -526,9 +527,11 @@ def __init__(self, a0, apx_tick_count=12): Parameters ---------- a0 : float - The scale parameter defining the extent of the quasi-linear region. + The scale parameter defining the extent + of the quasi-linear region. apx_tick_count : int, default: 12 - The approximate number of major ticks that will fit along the entire axis + The approximate number of major ticks that will fit + along the entire axis """ super().__init__() self.a0 = a0 @@ -539,12 +542,14 @@ def __call__(self): return self.tick_values(dmin, dmax) def tick_values(self, vmin, vmax): - # Construct a set of "on-screen" locations that are uniformly spaced: + # Construct a set of "on-screen" locations + # that are uniformly spaced: ymin, ymax = self.a0 * np.arcsinh(np.array([vmin, vmax]) / self.a0) ys = np.linspace(ymin, ymax, self.apx_tick_count) if (ymin * ymax) < 0: - # Ensure that zero tick-mark is included if the axis stradles zero - ys = np.hstack([ ys, 0.0 ]) + # Ensure that the zero tick-mark is included, + # if the axis stradles zero + ys = np.hstack([ys, 0.0]) # Transform the "on-screen" grid to the data space: xs = self.a0 * np.sinh(ys / self.a0) @@ -554,14 +559,14 @@ def tick_values(self, vmin, vmax): decades = ( np.where(xs >= 0, 1, -1) * np.power(10, np.where(zero_xs, 1.0, - np.floor(np.log10(np.abs(xs) + zero_xs*1e-6)))) + np.floor(np.log10(np.abs(xs) + + zero_xs*1e-6)))) ) qs = decades * np.round(xs / decades) return np.array(sorted(set(qs))) - class LogitTransform(Transform): input_dims = output_dims = 1 From 1ec0b229a837de41a3e54f21a54052c0fc2e70bb Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Thu, 30 Sep 2021 16:22:58 +0100 Subject: [PATCH 06/36] Improved documentation of asinh transformation and parameter naming --- examples/scales/asinh_demo.py | 24 +++++++++++-- lib/matplotlib/scale.py | 56 ++++++++++++++++++------------ lib/matplotlib/tests/test_scale.py | 5 +++ 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 19ffa373e3a7..b935590b113c 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -25,16 +25,34 @@ ax1.set_title(r'$sinh^{-1}$') -# Compare "asinh" graphs with different scale parameter "a0": +# Compare "asinh" graphs with different scale parameter "linear_width": fig2 = plt.figure() axs = fig2.subplots(1, 3, sharex=True) for ax, a0 in zip(axs, (0.2, 1.0, 5.0)): - ax.set_title('a0={:.3g}'.format(a0)) + 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', a0=a0) + ax.set_yscale('asinh', linear_width=a0) ax.grid() ax.legend(loc='best', fontsize='small') + +# Compare "symlog" and "asinh" scalings +# on 2D Cauchy-distributed random numbers: +fig3 = plt.figure() +ax = fig3.subplots(1, 1) +r = numpy.tan(numpy.random.uniform(-numpy.pi / 2.02, numpy.pi / 2.02, + size=(5000,))) +th = numpy.random.uniform(0, 2*numpy.pi, size=r.shape) + +ax.scatter(r * numpy.cos(th), r * numpy.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.grid() + + plt.show() diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 6817aa3d1b5c..88f2f6d17b5f 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -467,54 +467,64 @@ class AsinhScale(ScaleBase): logarithmic. The transition between these linear and logarithmic regimes is smooth, and has no discontinutities in the function gradient in contrast to the "symlog" scale. + + Specifically, the transformation of an axis coordinate :math:`a` is + is :math:`a \\rightarrow a_0 \sinh^{-1} (a / a_0)` where :math:`a_0` + is the effective width of the linear region of the transformation. + In that region, the transformation is + :math:`a \\rightarrow a + {\cal O}(a^3)`. + For large values of :math:`a` the transformation behaves as + :math:`a \\rightarrow a_0 \ln (a) + {\cal O}(1)`. """ name = 'asinh' - def __init__(self, axis, *, a0=1.0, **kwargs): + def __init__(self, axis, *, linear_width=1.0, **kwargs): """ Parameters ---------- - a0 : float, default: 1 - The scale parameter defining the extent of the quasi-linear region. + linear_width : float, default: 1 + The scale parameter defining the extent of the quasi-linear region, + and the coordinate values beyond which the transformation + becomes asympoticially logarithmic. """ super().__init__(axis) - if a0 <= 0.0: + if linear_width <= 0.0: raise ValueError("Scale parameter 'a0' must be strictly positive") - self.a0 = a0 + self.linear_width = linear_width def get_transform(self): - return self.AsinhTransform(self.a0) + return self.AsinhTransform(self.linear_width) def set_default_locators_and_formatters(self, axis): - axis.set(major_locator=AsinhScale.AsinhLocator(self.a0), + axis.set(major_locator=AsinhScale.AsinhLocator(self.linear_width), major_formatter='{x:.3g}') class AsinhTransform(Transform): input_dims = output_dims = 1 - def __init__(self, a0): + def __init__(self, linear_width): super().__init__() - self.a0 = a0 + self.linear_width = linear_width def transform_non_affine(self, a): - return self.a0 * np.arcsinh(a / self.a0) + return self.linear_width * np.arcsinh(a / self.linear_width) def inverted(self): - return AsinhScale.InvertedAsinhTransform(self.a0) + return AsinhScale.InvertedAsinhTransform(self.linear_width) class InvertedAsinhTransform(Transform): input_dims = output_dims = 1 - def __init__(self, a0): + def __init__(self, linear_width): super().__init__() - self.a0 = a0 + self.linear_width = linear_width def transform_non_affine(self, a): - return self.a0 * np.sinh(a / self.a0) + return self.linear_width * np.sinh(a / self.linear_width) def inverted(self): - return AsinhScale.AsinhTransform(self.a0) + return AsinhScale.AsinhTransform(self.linear_width) class AsinhLocator(Locator): """ @@ -522,20 +532,20 @@ class AsinhLocator(Locator): This is very unlikely to have any use beyond the AsinhScale class. """ - def __init__(self, a0, apx_tick_count=12): + def __init__(self, linear_width, numticks=12): """ Parameters ---------- - a0 : float + linear_width : float The scale parameter defining the extent of the quasi-linear region. - apx_tick_count : int, default: 12 + numticks : int, default: 12 The approximate number of major ticks that will fit along the entire axis """ super().__init__() - self.a0 = a0 - self.apx_tick_count = apx_tick_count + self.linear_width = linear_width + self.numticks = numticks def __call__(self): dmin, dmax = self.axis.get_data_interval() @@ -544,15 +554,15 @@ def __call__(self): def tick_values(self, vmin, vmax): # Construct a set of "on-screen" locations # that are uniformly spaced: - ymin, ymax = self.a0 * np.arcsinh(np.array([vmin, vmax]) / self.a0) - ys = np.linspace(ymin, ymax, self.apx_tick_count) + ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax]) / self.linear_width) + ys = np.linspace(ymin, ymax, self.numticks) if (ymin * ymax) < 0: # Ensure that the zero tick-mark is included, # if the axis stradles zero ys = np.hstack([ys, 0.0]) # Transform the "on-screen" grid to the data space: - xs = self.a0 * np.sinh(ys / self.a0) + xs = self.linear_width * np.sinh(ys / self.linear_width) zero_xs = (xs == 0) # Round the data-space values to be intuitive decimal numbers: diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 8fba86d2e82e..da43a463c9f7 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -219,3 +219,8 @@ def test_scale_deepcopy(): sc2 = copy.deepcopy(sc) assert str(sc.get_transform()) == str(sc2.get_transform()) assert sc._transform is not sc2._transform + + +def test_asinh_transforms(): + # FIXME - more here soon + pass From 2b0d58836f1b67689db4f79e053fd2ce33e29dae Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Thu, 30 Sep 2021 16:55:22 +0100 Subject: [PATCH 07/36] Moved AsinhLocator into ticker.py and extracted AsinhScale nested transform classes --- lib/matplotlib/scale.py | 111 ++++++++--------------------- lib/matplotlib/tests/test_scale.py | 16 ++++- lib/matplotlib/ticker.py | 53 ++++++++++++++ 3 files changed, 98 insertions(+), 82 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 88f2f6d17b5f..c37286af6ed2 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -23,7 +23,7 @@ from matplotlib.ticker import ( NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter, Locator, NullLocator, LogLocator, AutoLocator, AutoMinorLocator, - SymmetricalLogLocator, LogitLocator) + SymmetricalLogLocator, AsinhLocator, LogitLocator) from matplotlib.transforms import Transform, IdentityTransform @@ -458,6 +458,34 @@ def get_transform(self): return self._transform +class AsinhTransform(Transform): + input_dims = output_dims = 1 + + def __init__(self, linear_width): + super().__init__() + self.linear_width = linear_width + + def transform_non_affine(self, a): + return self.linear_width * np.arcsinh(a / self.linear_width) + + def inverted(self): + return InvertedAsinhTransform(self.linear_width) + + +class InvertedAsinhTransform(Transform): + input_dims = output_dims = 1 + + def __init__(self, linear_width): + super().__init__() + self.linear_width = linear_width + + def transform_non_affine(self, a): + return self.linear_width * np.sinh(a / self.linear_width) + + def inverted(self): + return AsinhTransform(self.linear_width) + + class AsinhScale(ScaleBase): """ A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh) @@ -494,89 +522,12 @@ def __init__(self, axis, *, linear_width=1.0, **kwargs): self.linear_width = linear_width def get_transform(self): - return self.AsinhTransform(self.linear_width) + return AsinhTransform(self.linear_width) def set_default_locators_and_formatters(self, axis): - axis.set(major_locator=AsinhScale.AsinhLocator(self.linear_width), + axis.set(major_locator=AsinhLocator(self.linear_width), major_formatter='{x:.3g}') - class AsinhTransform(Transform): - input_dims = output_dims = 1 - - def __init__(self, linear_width): - super().__init__() - self.linear_width = linear_width - - def transform_non_affine(self, a): - return self.linear_width * np.arcsinh(a / self.linear_width) - - def inverted(self): - return AsinhScale.InvertedAsinhTransform(self.linear_width) - - class InvertedAsinhTransform(Transform): - input_dims = output_dims = 1 - - def __init__(self, linear_width): - super().__init__() - self.linear_width = linear_width - - def transform_non_affine(self, a): - return self.linear_width * np.sinh(a / self.linear_width) - - def inverted(self): - return AsinhScale.AsinhTransform(self.linear_width) - - class AsinhLocator(Locator): - """ - An axis tick locator specialized for the arcsinh scale - - This is very unlikely to have any use beyond the AsinhScale class. - """ - def __init__(self, linear_width, numticks=12): - """ - Parameters - ---------- - linear_width : float - The scale parameter defining the extent - of the quasi-linear region. - numticks : int, default: 12 - The approximate number of major ticks that will fit - along the entire axis - """ - super().__init__() - self.linear_width = linear_width - self.numticks = numticks - - def __call__(self): - dmin, dmax = self.axis.get_data_interval() - return self.tick_values(dmin, dmax) - - def tick_values(self, vmin, vmax): - # Construct a set of "on-screen" locations - # that are uniformly spaced: - ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax]) / self.linear_width) - ys = np.linspace(ymin, ymax, self.numticks) - if (ymin * ymax) < 0: - # Ensure that the zero tick-mark is included, - # if the axis stradles zero - ys = np.hstack([ys, 0.0]) - - # Transform the "on-screen" grid to the data space: - xs = self.linear_width * np.sinh(ys / self.linear_width) - zero_xs = (xs == 0) - - # Round the data-space values to be intuitive decimal numbers: - decades = ( - np.where(xs >= 0, 1, -1) * - np.power(10, np.where(zero_xs, 1.0, - np.floor(np.log10(np.abs(xs) - + zero_xs*1e-6)))) - ) - qs = decades * np.round(xs / decades) - - return np.array(sorted(set(qs))) - - class LogitTransform(Transform): input_dims = output_dims = 1 diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index da43a463c9f7..feb476989c10 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -2,6 +2,7 @@ import matplotlib.pyplot as plt from matplotlib.scale import ( + AsinhTransform, LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale @@ -222,5 +223,16 @@ def test_scale_deepcopy(): def test_asinh_transforms(): - # FIXME - more here soon - pass + a0 = 17.0 + a = np.linspace(-50, 50, 100) + + forward = AsinhTransform(a0) + inverse = forward.inverted() + invinv = inverse.inverted() + + a_forward = forward.transform_non_affine(a) + a_inverted = inverse.transform_non_affine(a_forward) + assert_allclose(a_inverted, a) + + a_invinv = invinv.transform_non_affine(a) + assert_allclose(a_invinv, a0 * numpy.asinh(a / a0)) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 6d8fa5419bbf..a39085b0fd6c 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -36,6 +36,8 @@ `SymmetricalLogLocator` Locator for use with with the symlog norm; works like `LogLocator` for the part outside of the threshold and adds 0 if inside the limits. +`AsinhLocator` Locator for use with the asinh norm, attempting to + space ticks approximately uniformly. `LogitLocator` Locator for logit scaling. `AutoMinorLocator` Locator for minor ticks when the axis is linear and the major ticks are uniformly spaced. Subdivides the major @@ -2583,6 +2585,57 @@ def view_limits(self, vmin, vmax): return result +class AsinhLocator(Locator): + """ + An axis tick locator specialized for the inverse-sinh scale + + This is very unlikely to have any use beyond the AsinhScale class. + """ + def __init__(self, linear_width, numticks=12): + """ + Parameters + ---------- + linear_width : float + The scale parameter defining the extent + of the quasi-linear region. + numticks : int, default: 12 + The approximate number of major ticks that will fit + along the entire axis + """ + super().__init__() + self.linear_width = linear_width + self.numticks = numticks + + def __call__(self): + dmin, dmax = self.axis.get_data_interval() + return self.tick_values(dmin, dmax) + + def tick_values(self, vmin, vmax): + # Construct a set of "on-screen" locations + # that are uniformly spaced: + ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax]) / self.linear_width) + ys = np.linspace(ymin, ymax, self.numticks) + if (ymin * ymax) < 0: + # Ensure that the zero tick-mark is included, + # if the axis stradles zero + ys = np.hstack([ys, 0.0]) + + # Transform the "on-screen" grid to the data space: + xs = self.linear_width * np.sinh(ys / self.linear_width) + zero_xs = (xs == 0) + + # Round the data-space values to be intuitive decimal numbers: + decades = ( + np.where(xs >= 0, 1, -1) * + np.power(10, np.where(zero_xs, 1.0, + np.floor(np.log10(np.abs(xs) + + zero_xs*1e-6)))) + ) + qs = decades * np.round(xs / decades) + + return np.array(sorted(set(qs))) + + class LogitLocator(MaxNLocator): """ Determine the tick locations for logit axes From 2002cb34d641f811649126c2d9b368fcbc5470b7 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Thu, 30 Sep 2021 17:20:46 +0100 Subject: [PATCH 08/36] Added set_params() method to AsinhLocator --- lib/matplotlib/tests/test_ticker.py | 8 ++++++++ lib/matplotlib/ticker.py | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index d99fd5a306b9..cfc62f226d2a 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -443,6 +443,14 @@ def test_set_params(self): assert sym.numticks == 8 +class TestAsinhLocator: + def test_set_params(self): + lctr = mticker.AsinhLocator(linear_width=5, numticks=17) + assert lctr.numticks == 17 + lctr.set_params(numticks=23) + assert lctr.numticks == 23 + + class TestScalarFormatter: offset_data = [ (123, 189, 0), diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index a39085b0fd6c..b6d7f4bf6efc 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -148,7 +148,7 @@ 'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator', 'LinearLocator', 'LogLocator', 'AutoLocator', 'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator', - 'SymmetricalLogLocator', 'LogitLocator') + 'SymmetricalLogLocator', 'AsinhLocator', 'LogitLocator') class _DummyAxis: @@ -2606,6 +2606,11 @@ def __init__(self, linear_width, numticks=12): self.linear_width = linear_width self.numticks = numticks + def set_params(self, numticks=None): + """Set parameters within this locator.""" + if numticks is not None: + self.numticks = numticks + def __call__(self): dmin, dmax = self.axis.get_data_interval() return self.tick_values(dmin, dmax) From 070d984060ce86812a519428fd29bb86b48fa7d0 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Thu, 30 Sep 2021 18:10:59 +0100 Subject: [PATCH 09/36] Patched asinh-transforms test numpy namespace --- lib/matplotlib/tests/test_scale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index feb476989c10..6256be7a8671 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -235,4 +235,4 @@ def test_asinh_transforms(): assert_allclose(a_inverted, a) a_invinv = invinv.transform_non_affine(a) - assert_allclose(a_invinv, a0 * numpy.asinh(a / a0)) + assert_allclose(a_invinv, a0 * np.asinh(a / a0)) From 8495f7b2f488c9a97ce46cdf019a51226c49ef75 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Fri, 1 Oct 2021 06:49:44 +0100 Subject: [PATCH 10/36] Fixed and refactored various AsinhScale tests --- lib/matplotlib/scale.py | 2 +- lib/matplotlib/tests/test_scale.py | 45 +++++++++++++++++++++-------- lib/matplotlib/tests/test_ticker.py | 7 +++++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index c37286af6ed2..d0a9d6a1d4d7 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -22,7 +22,7 @@ from matplotlib import _api, docstring from matplotlib.ticker import ( NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter, - Locator, NullLocator, LogLocator, AutoLocator, AutoMinorLocator, + NullLocator, LogLocator, AutoLocator, AutoMinorLocator, SymmetricalLogLocator, AsinhLocator, LogitLocator) from matplotlib.transforms import Transform, IdentityTransform diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 6256be7a8671..51da63da80a7 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -2,7 +2,7 @@ import matplotlib.pyplot as plt from matplotlib.scale import ( - AsinhTransform, + AsinhScale, AsinhTransform, LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale @@ -222,17 +222,38 @@ def test_scale_deepcopy(): assert sc._transform is not sc2._transform -def test_asinh_transforms(): - a0 = 17.0 - a = np.linspace(-50, 50, 100) +class TestAsinhScale: + def test_transforms(self): + a0 = 17.0 + a = np.linspace(-50, 50, 100) - forward = AsinhTransform(a0) - inverse = forward.inverted() - invinv = inverse.inverted() + forward = AsinhTransform(a0) + inverse = forward.inverted() + invinv = inverse.inverted() - a_forward = forward.transform_non_affine(a) - a_inverted = inverse.transform_non_affine(a_forward) - assert_allclose(a_inverted, a) + a_forward = forward.transform_non_affine(a) + a_inverted = inverse.transform_non_affine(a_forward) + assert_allclose(a_inverted, a) - a_invinv = invinv.transform_non_affine(a) - assert_allclose(a_invinv, a0 * np.asinh(a / a0)) + a_invinv = invinv.transform_non_affine(a) + assert_allclose(a_invinv, a0 * np.arcsinh(a / a0)) + + def test_init(self): + fig, ax = plt.subplots() + + s = AsinhScale(axis=None, linear_width=23.0) + assert s.linear_width == 23 + + tx = s.get_transform() + assert isinstance(tx, AsinhTransform) + assert tx.linear_width == s.linear_width + + def test_bad_scale(self): + fig, ax = plt.subplots() + + with pytest.raises(ValueError): + AsinhScale(axis=None, linear_width=0) + with pytest.raises(ValueError): + AsinhScale(axis=None, linear_width=-1) + s0 = AsinhScale(axis=None, ) + s1 = AsinhScale(axis=None, linear_width=3.0) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index cfc62f226d2a..470c6ce80cd1 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -444,11 +444,18 @@ def test_set_params(self): class TestAsinhLocator: + def test_init(self): + lctr = mticker.AsinhLocator(linear_width=2.718, numticks=19) + assert lctr.linear_width == 2.718 + assert lctr.numticks == 19 + def test_set_params(self): lctr = mticker.AsinhLocator(linear_width=5, numticks=17) assert lctr.numticks == 17 lctr.set_params(numticks=23) assert lctr.numticks == 23 + lctr.set_params(None) + assert lctr.numticks == 23 class TestScalarFormatter: From 279c38b03881471e9ed0e06546adaf489a5ba015 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Fri, 1 Oct 2021 07:42:57 +0100 Subject: [PATCH 11/36] Added tests for Asinh tick locations and improved handling of zero-straddling --- lib/matplotlib/tests/test_ticker.py | 20 ++++++++++++++++++++ lib/matplotlib/ticker.py | 10 ++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 470c6ce80cd1..3650e1c7b9b5 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -457,6 +457,26 @@ def test_set_params(self): lctr.set_params(None) assert lctr.numticks == 23 + def test_linear_values(self): + lctr = mticker.AsinhLocator(linear_width=100, numticks=11) + + assert_almost_equal(lctr.tick_values(-1, 1), + np.arange(-1, 1.01, 0.2)) + assert_almost_equal(lctr.tick_values(-0.1, 0.1), + np.arange(-0.1, 0.101, 0.02)) + assert_almost_equal(lctr.tick_values(-0.01, 0.01), + np.arange(-0.01, 0.0101, 0.002)) + + def test_wide_values(self): + lctr = mticker.AsinhLocator(linear_width=0.1, numticks=11) + + assert_almost_equal(lctr.tick_values(-100, 100), + [-100, -20, -5, -1, -0.2, + 0, 0.2, 1, 5, 20, 100]) + assert_almost_equal(lctr.tick_values(-1000, 1000), + [-1000, -100, -20, -3, -0.4, + 0, 0.4, 3, 20, 100, 1000]) + class TestScalarFormatter: offset_data = [ diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index b6d7f4bf6efc..0c13a1fc9243 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2591,7 +2591,7 @@ class AsinhLocator(Locator): This is very unlikely to have any use beyond the AsinhScale class. """ - def __init__(self, linear_width, numticks=12): + def __init__(self, linear_width, numticks=11): """ Parameters ---------- @@ -2618,12 +2618,14 @@ def __call__(self): def tick_values(self, vmin, vmax): # Construct a set of "on-screen" locations # that are uniformly spaced: - ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax]) / self.linear_width) + ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax]) + / self.linear_width) ys = np.linspace(ymin, ymax, self.numticks) - if (ymin * ymax) < 0: + zero_dev = np.abs(ys / (ymax - ymin)) + if (ymin * ymax) < 0 and min(zero_dev) > 1e-6: # Ensure that the zero tick-mark is included, # if the axis stradles zero - ys = np.hstack([ys, 0.0]) + ys = np.hstack([ys[(zero_dev > 0.5 / self.numticks)], 0.0]) # Transform the "on-screen" grid to the data space: xs = self.linear_width * np.sinh(ys / self.linear_width) From 76d9b42636b179b347d1f58bb2428701ae2123f1 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Fri, 1 Oct 2021 10:48:42 +0100 Subject: [PATCH 12/36] Improved overview documentation --- doc/users/next_whats_new/asinh_scale.rst | 26 +++++++++++++++- examples/scales/asinh_demo.py | 38 ++++++++++++++++++++++-- lib/matplotlib/scale.py | 6 ++-- lib/matplotlib/ticker.py | 5 +++- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/doc/users/next_whats_new/asinh_scale.rst b/doc/users/next_whats_new/asinh_scale.rst index 1756111ba59b..fa48b4117182 100644 --- a/doc/users/next_whats_new/asinh_scale.rst +++ b/doc/users/next_whats_new/asinh_scale.rst @@ -5,5 +5,29 @@ 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 than span many orders -of magnitude. A scale parameter ``a0`` is provided to allow the user +of magnitude. A scale parameter is provided to allow the user to tune the width of the linear region of the scale. + +.. plot:: + + from matplotlib import pyplot as plt + import numpy + + fig, (ax0, ax1) = plt.subplots(1, 2, sharex=True) + x = numpy.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) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index b935590b113c..4a4435dac17c 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -2,6 +2,37 @@ ============ Asinh Demo ============ + +Illustration of the `asinh` 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 \\ln (a) + {\\cal O}(1) + +As with the `symlog` 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 tranformation +that has discontinuities in its gradient because it is built +from *separate* linear and logarithmic transformation. +The `asinh` scaling uses a transformation that is smooth +for all (finite) values, which is both mathematically cleaner +and should reduce visual artifacts associated with an abrupt +transition between linear and logarithmic regions of the plot. + +See `~.scale.AsinhScale`, `~.scale.SymmetricalLogScale`. """ import numpy @@ -10,6 +41,7 @@ # Prepare sample values for variations on y=x graph: x = numpy.linspace(-3, 6, 100) +######################################## # Compare "symlog" and "asinh" behaviour on sample y=x graph: fig1 = plt.figure() ax0, ax1 = fig1.subplots(1, 2, sharex=True) @@ -22,12 +54,13 @@ ax1.plot(x, x) ax1.set_yscale('asinh') ax1.grid() -ax1.set_title(r'$sinh^{-1}$') +ax1.set_title('asinh') +######################################## # Compare "asinh" graphs with different scale parameter "linear_width": fig2 = plt.figure() -axs = fig2.subplots(1, 3, sharex=True) +axs = fig2.subplots(1, 3, sharex=True, constrained_layout=True) for ax, a0 in zip(axs, (0.2, 1.0, 5.0)): ax.set_title('linear_width={:.3g}'.format(a0)) ax.plot(x, x, label='y=x') @@ -38,6 +71,7 @@ ax.legend(loc='best', fontsize='small') +######################################## # Compare "symlog" and "asinh" scalings # on 2D Cauchy-distributed random numbers: fig3 = plt.figure() diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index d0a9d6a1d4d7..ea0f32513355 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -497,12 +497,12 @@ class AsinhScale(ScaleBase): in contrast to the "symlog" scale. Specifically, the transformation of an axis coordinate :math:`a` is - is :math:`a \\rightarrow a_0 \sinh^{-1} (a / a_0)` where :math:`a_0` + is :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0` is the effective width of the linear region of the transformation. In that region, the transformation is - :math:`a \\rightarrow a + {\cal O}(a^3)`. + :math:`a \\rightarrow a + {\\cal O}(a^3)`. For large values of :math:`a` the transformation behaves as - :math:`a \\rightarrow a_0 \ln (a) + {\cal O}(1)`. + :math:`a \\rightarrow a_0 \\ln (a) + {\\cal O}(1)`. """ name = 'asinh' diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 0c13a1fc9243..f29f31cef4dd 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2640,7 +2640,10 @@ def tick_values(self, vmin, vmax): ) qs = decades * np.round(xs / decades) - return np.array(sorted(set(qs))) + if len(qs) > self.numticks // 2: + return np.array(sorted(set(qs))) + else: + return np.linspace(vmin, vmax, self.numticks) class LogitLocator(MaxNLocator): From 8143153196ee585d3dc3d1f2b7d897d5a888c292 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Fri, 1 Oct 2021 14:19:32 +0100 Subject: [PATCH 13/36] Improved testing of locator edge-cases, and patched constrained_layout misstep --- examples/scales/asinh_demo.py | 4 ++-- lib/matplotlib/scale.py | 3 ++- lib/matplotlib/tests/test_ticker.py | 12 ++++++++++++ lib/matplotlib/ticker.py | 5 +++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 4a4435dac17c..edb7dac219ab 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -59,8 +59,8 @@ ######################################## # Compare "asinh" graphs with different scale parameter "linear_width": -fig2 = plt.figure() -axs = fig2.subplots(1, 3, sharex=True, constrained_layout=True) +fig2 = plt.figure(constrained_layout=True) +axs = fig2.subplots(1, 3, sharex=True) for ax, a0 in zip(axs, (0.2, 1.0, 5.0)): ax.set_title('linear_width={:.3g}'.format(a0)) ax.plot(x, x, label='y=x') diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index ea0f32513355..4999f5935fc1 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -512,7 +512,8 @@ def __init__(self, axis, *, linear_width=1.0, **kwargs): Parameters ---------- linear_width : float, default: 1 - The scale parameter defining the extent of the quasi-linear region, + The scale parameter (elsewhere referred to as :math:`a_0`) + defining the extent of the quasi-linear region, and the coordinate values beyond which the transformation becomes asympoticially logarithmic. """ diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 3650e1c7b9b5..befb7dd53dba 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -477,6 +477,18 @@ def test_wide_values(self): [-1000, -100, -20, -3, -0.4, 0, 0.4, 3, 20, 100, 1000]) + def test_near_zero(self): + """Check that manually injected zero will supersede nearby tick""" + lctr = mticker.AsinhLocator(linear_width=100, numticks=3) + + assert_almost_equal(lctr.tick_values(-1.1, 0.9), [ -1.0, 0.0, 0.9]) + + def test_fallback(self): + lctr = mticker.AsinhLocator(1.0, numticks=11) + + assert_almost_equal(lctr.tick_values(100, 101), + np.arange(100, 101.01, 0.1)) + class TestScalarFormatter: offset_data = [ diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index f29f31cef4dd..c63002463647 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2639,9 +2639,10 @@ def tick_values(self, vmin, vmax): + zero_xs*1e-6)))) ) qs = decades * np.round(xs / decades) + ticks = np.array(sorted(set(qs))) - if len(qs) > self.numticks // 2: - return np.array(sorted(set(qs))) + if len(ticks) > self.numticks // 2: + return ticks else: return np.linspace(vmin, vmax, self.numticks) From 800ef38de533328704b21795d712f2440d152d06 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 3 Oct 2021 06:20:30 +0100 Subject: [PATCH 14/36] Minor corrections to documentation --- examples/scales/asinh_demo.py | 25 +++++++++++++++---------- lib/matplotlib/scale.py | 12 +++++++----- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index edb7dac219ab..7a32924ce9d4 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -20,13 +20,13 @@ .. math:: - a \\rightarrow a_0 \\ln (a) + {\\cal O}(1) + a \\rightarrow a_0 \\, {\\rm sgn}(a) \\ln |a| + {\\cal O}(1) As with the `symlog` 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 tranformation that has discontinuities in its gradient because it is built -from *separate* linear and logarithmic transformation. +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 should reduce visual artifacts associated with an abrupt @@ -35,14 +35,15 @@ See `~.scale.AsinhScale`, `~.scale.SymmetricalLogScale`. """ -import numpy +import numpy as np import matplotlib.pyplot as plt # Prepare sample values for variations on y=x graph: -x = numpy.linspace(-3, 6, 100) +x = np.linspace(-3, 6, 100) ######################################## -# Compare "symlog" and "asinh" behaviour on sample y=x graph: +# Compare "symlog" and "asinh" behaviour on sample y=x graph, +# where the discontinuous gradient in "symlog" near y=2 is obvious: fig1 = plt.figure() ax0, ax1 = fig1.subplots(1, 2, sharex=True) @@ -73,19 +74,23 @@ ######################################## # Compare "symlog" and "asinh" scalings -# on 2D Cauchy-distributed random numbers: +# 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 = numpy.tan(numpy.random.uniform(-numpy.pi / 2.02, numpy.pi / 2.02, - size=(5000,))) -th = numpy.random.uniform(0, 2*numpy.pi, size=r.shape) +r = 3 * np.tan(np.random.uniform(-np.pi / 2.01, np.pi / 2.01, + size=(5000,))) +th = np.random.uniform(0, 2*np.pi, size=r.shape) -ax.scatter(r * numpy.cos(th), r * numpy.sin(th), s=4, alpha=0.5) +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() diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 4999f5935fc1..f0af4b7195d9 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -459,6 +459,7 @@ def get_transform(self): class AsinhTransform(Transform): + """Inverse hyperbolic-sine transformation used by `.AsinhScale`""" input_dims = output_dims = 1 def __init__(self, linear_width): @@ -473,6 +474,7 @@ def inverted(self): class InvertedAsinhTransform(Transform): + """Hyperbolic-sine transformation used by `.AsinhScale`""" input_dims = output_dims = 1 def __init__(self, linear_width): @@ -493,16 +495,16 @@ class AsinhScale(ScaleBase): For values close to zero, this is essentially a linear scale, but for larger values (either positive or negative) it is asymptotically logarithmic. The transition between these linear and logarithmic regimes - is smooth, and has no discontinutities in the function gradient - in contrast to the "symlog" scale. + is smooth, and has no discontinuities in the function gradient + in contrast to the `symlog` scale. Specifically, the transformation of an axis coordinate :math:`a` is - is :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0` + :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0` is the effective width of the linear region of the transformation. In that region, the transformation is :math:`a \\rightarrow a + {\\cal O}(a^3)`. For large values of :math:`a` the transformation behaves as - :math:`a \\rightarrow a_0 \\ln (a) + {\\cal O}(1)`. + :math:`a \\rightarrow a_0 \\, {\\rm sgn}(a) \\ln |a| + {\\cal O}(1)`. """ name = 'asinh' @@ -515,7 +517,7 @@ def __init__(self, axis, *, linear_width=1.0, **kwargs): The scale parameter (elsewhere referred to as :math:`a_0`) defining the extent of the quasi-linear region, and the coordinate values beyond which the transformation - becomes asympoticially logarithmic. + becomes asympotically logarithmic. """ super().__init__(axis) if linear_width <= 0.0: From 0ad43a52f2ada6ab8b81176f9c0c74b5719e0565 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 3 Oct 2021 08:20:15 +0100 Subject: [PATCH 15/36] Improved handling of data ranges almost symmetrical about zero --- examples/scales/asinh_demo.py | 12 ++++++----- lib/matplotlib/scale.py | 2 +- lib/matplotlib/tests/test_ticker.py | 32 ++++++++++++++++++++++++++++- lib/matplotlib/ticker.py | 27 +++++++++++++++++------- 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 7a32924ce9d4..8212d8f021e6 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -3,7 +3,8 @@ Asinh Demo ============ -Illustration of the `asinh` axis scaling, which uses the transformation +Illustration of the `asinh <.scale.AsinhScale>` axis scaling, +which uses the transformation .. math:: @@ -22,12 +23,13 @@ a \\rightarrow a_0 \\, {\\rm sgn}(a) \\ln |a| + {\\cal O}(1) -As with the `symlog` scaling, this allows one to plot quantities +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 tranformation +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 +The ``asinh`` scaling uses a transformation that is smooth for all (finite) values, which is both mathematically cleaner and should reduce visual artifacts associated with an abrupt transition between linear and logarithmic regions of the plot. @@ -79,7 +81,7 @@ # 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.01, np.pi / 2.01, +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) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index f0af4b7195d9..6938c2faa438 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -496,7 +496,7 @@ class AsinhScale(ScaleBase): but for larger values (either positive or negative) it is asymptotically logarithmic. The transition between these linear and logarithmic regimes is smooth, and has no discontinuities in the function gradient - in contrast to the `symlog` scale. + in contrast to the `.SymmetricalLogScale` ("symlog") scale. Specifically, the transformation of an axis coordinate :math:`a` is :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0` diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index befb7dd53dba..dc3c5e959c80 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -450,13 +450,21 @@ def test_init(self): assert lctr.numticks == 19 def test_set_params(self): - lctr = mticker.AsinhLocator(linear_width=5, numticks=17) + lctr = mticker.AsinhLocator(linear_width=5, + numticks=17, symthresh=0.125) assert lctr.numticks == 17 + assert lctr.symthresh == 0.125 + lctr.set_params(numticks=23) assert lctr.numticks == 23 lctr.set_params(None) assert lctr.numticks == 23 + lctr.set_params(symthresh=0.5) + assert lctr.symthresh == 0.5 + lctr.set_params(symthresh=None) + assert lctr.symthresh == 0.5 + def test_linear_values(self): lctr = mticker.AsinhLocator(linear_width=100, numticks=11) @@ -489,6 +497,28 @@ def test_fallback(self): assert_almost_equal(lctr.tick_values(100, 101), np.arange(100, 101.01, 0.1)) + def test_symmetrizing(self): + class DummyAxis: + bounds = (-1, 1) + @classmethod + def get_data_interval(cls): return cls.bounds + + lctr = mticker.AsinhLocator(linear_width=1, numticks=3, + symthresh=0.25) + lctr.axis = DummyAxis + + DummyAxis.bounds = (-1, 2) + assert_almost_equal(lctr(), [-1, 0, 2]) + + DummyAxis.bounds = (-1, 0.9) + assert_almost_equal(lctr(), [-1, 0, 1]) + + DummyAxis.bounds = (-0.85, 1.05) + assert_almost_equal(lctr(), [-1, 0, 1]) + + DummyAxis.bounds = (1, 1.1) + assert_almost_equal(lctr(), [1, 1.05, 1.1]) + class TestScalarFormatter: offset_data = [ diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index c63002463647..e285f81df0bf 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2589,31 +2589,44 @@ class AsinhLocator(Locator): """ An axis tick locator specialized for the inverse-sinh scale - This is very unlikely to have any use beyond the AsinhScale class. + This is very unlikely to have any use beyond + the `~.scale.AsinhScale` class. """ - def __init__(self, linear_width, numticks=11): + def __init__(self, linear_width, numticks=11, symthresh=0.2): """ Parameters ---------- linear_width : float The scale parameter defining the extent of the quasi-linear region. - numticks : int, default: 12 + numticks : int, default: 11 The approximate number of major ticks that will fit along the entire axis + symthresh : float, default: 0.2 + The fractional threshold beneath which data which covers + a range that is approximately symmetric about zero + will have ticks that are exactly symmetric. """ super().__init__() self.linear_width = linear_width self.numticks = numticks + self.symthresh = symthresh - def set_params(self, numticks=None): + def set_params(self, numticks=None, symthresh=None): """Set parameters within this locator.""" if numticks is not None: self.numticks = numticks + if symthresh is not None: + self.symthresh = symthresh def __call__(self): dmin, dmax = self.axis.get_data_interval() - return self.tick_values(dmin, dmax) + if (dmin * dmax) < 0 and abs(1 + dmax / dmin) < self.symthresh: + # Data-range appears to be almost symmetric, so round up: + bound = max(abs(dmin), abs(dmax)) + return self.tick_values(-bound, bound) + else: + return self.tick_values(dmin, dmax) def tick_values(self, vmin, vmax): # Construct a set of "on-screen" locations @@ -2622,9 +2635,9 @@ def tick_values(self, vmin, vmax): / self.linear_width) ys = np.linspace(ymin, ymax, self.numticks) zero_dev = np.abs(ys / (ymax - ymin)) - if (ymin * ymax) < 0 and min(zero_dev) > 1e-6: + if (ymin * ymax) < 0 and min(zero_dev) > 0: # Ensure that the zero tick-mark is included, - # if the axis stradles zero + # if the axis straddles zero ys = np.hstack([ys[(zero_dev > 0.5 / self.numticks)], 0.0]) # Transform the "on-screen" grid to the data space: From ba597de2060e05f61e63c40a1afafe99fac2639b Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 3 Oct 2021 14:41:28 +0100 Subject: [PATCH 16/36] Added AutoLocator for minor ticks and further tweaks of zero-crossing logic --- examples/scales/asinh_demo.py | 2 +- lib/matplotlib/scale.py | 4 +++- lib/matplotlib/ticker.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 8212d8f021e6..25ad52352c52 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -41,7 +41,7 @@ import matplotlib.pyplot as plt # Prepare sample values for variations on y=x graph: -x = np.linspace(-3, 6, 100) +x = np.linspace(-3, 6, 500) ######################################## # Compare "symlog" and "asinh" behaviour on sample y=x graph, diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 6938c2faa438..f272db286366 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -529,7 +529,9 @@ def get_transform(self): def set_default_locators_and_formatters(self, axis): axis.set(major_locator=AsinhLocator(self.linear_width), - major_formatter='{x:.3g}') + minor_locator=AutoLocator(), + major_formatter='{x:.3g}', + minor_formatter=NullFormatter()) class LogitTransform(Transform): input_dims = output_dims = 1 diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index e285f81df0bf..59ae0caf0363 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2635,7 +2635,7 @@ def tick_values(self, vmin, vmax): / self.linear_width) ys = np.linspace(ymin, ymax, self.numticks) zero_dev = np.abs(ys / (ymax - ymin)) - if (ymin * ymax) < 0 and min(zero_dev) > 0: + if (ymin * ymax) < 0: # Ensure that the zero tick-mark is included, # if the axis straddles zero ys = np.hstack([ys[(zero_dev > 0.5 / self.numticks)], 0.0]) @@ -2647,7 +2647,7 @@ def tick_values(self, vmin, vmax): # Round the data-space values to be intuitive decimal numbers: decades = ( np.where(xs >= 0, 1, -1) * - np.power(10, np.where(zero_xs, 1.0, + np.power(10, np.where(zero_xs, 0.0, np.floor(np.log10(np.abs(xs) + zero_xs*1e-6)))) ) From 80f2600b6588852f7aabf019f073f12087d063ea Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Tue, 5 Oct 2021 17:37:28 +0100 Subject: [PATCH 17/36] Patched flake8 slip-ups --- examples/scales/asinh_demo.py | 2 +- lib/matplotlib/scale.py | 1 + lib/matplotlib/tests/test_ticker.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 25ad52352c52..531b58445e55 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -17,7 +17,7 @@ a \\rightarrow a + {\\cal O}(a^3) -but for larger values (i.e. :math:`|a| \gg a_0`, this is asymptotically +but for larger values (i.e. :math:`|a| \\gg a_0`, this is asymptotically .. math:: diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index f272db286366..e3d33b21769f 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -533,6 +533,7 @@ def set_default_locators_and_formatters(self, axis): major_formatter='{x:.3g}', minor_formatter=NullFormatter()) + class LogitTransform(Transform): input_dims = output_dims = 1 diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index dc3c5e959c80..ef42f87624d7 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -489,7 +489,7 @@ def test_near_zero(self): """Check that manually injected zero will supersede nearby tick""" lctr = mticker.AsinhLocator(linear_width=100, numticks=3) - assert_almost_equal(lctr.tick_values(-1.1, 0.9), [ -1.0, 0.0, 0.9]) + assert_almost_equal(lctr.tick_values(-1.1, 0.9), [-1.0, 0.0, 0.9]) def test_fallback(self): lctr = mticker.AsinhLocator(1.0, numticks=11) From 65eff091b35fda2dc9af3103d74df062cf732665 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sat, 16 Oct 2021 09:07:45 +0100 Subject: [PATCH 18/36] Reworked AsinhLocator to allow rounding on arbitrary number base --- lib/matplotlib/scale.py | 27 ++++++++++++++++++-------- lib/matplotlib/ticker.py | 41 ++++++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index e3d33b21769f..1462ef6f370b 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -464,6 +464,8 @@ class AsinhTransform(Transform): def __init__(self, linear_width): super().__init__() + if linear_width <= 0.0: + raise ValueError("Scale parameter 'a0' must be strictly positive") self.linear_width = linear_width def transform_non_affine(self, a): @@ -509,7 +511,8 @@ class AsinhScale(ScaleBase): name = 'asinh' - def __init__(self, axis, *, linear_width=1.0, **kwargs): + def __init__(self, axis, *, linear_width=1.0, + base=10, subs=(2, 5), **kwargs): """ Parameters ---------- @@ -520,18 +523,26 @@ def __init__(self, axis, *, linear_width=1.0, **kwargs): becomes asympotically logarithmic. """ super().__init__(axis) - if linear_width <= 0.0: - raise ValueError("Scale parameter 'a0' must be strictly positive") - self.linear_width = linear_width + self._transform = AsinhTransform(linear_width) + self._base = int(base) + self._subs = subs + + linear_width = property(lambda self: self._transform.linear_width) def get_transform(self): - return AsinhTransform(self.linear_width) + return self._transform def set_default_locators_and_formatters(self, axis): - axis.set(major_locator=AsinhLocator(self.linear_width), - minor_locator=AutoLocator(), - major_formatter='{x:.3g}', + axis.set(major_locator=AsinhLocator(self.linear_width, + base=self._base), + minor_locator=AsinhLocator(self.linear_width, + base=self._base, + subs=self._subs), minor_formatter=NullFormatter()) + if self._base > 1: + axis.set_major_formatter(LogFormatterSciNotation(self._base)) + else: + axis.set_major_formatter('{x:.3g}'), class LogitTransform(Transform): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 59ae0caf0363..a0527c4d3fa3 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2592,7 +2592,8 @@ class AsinhLocator(Locator): This is very unlikely to have any use beyond the `~.scale.AsinhScale` class. """ - def __init__(self, linear_width, numticks=11, symthresh=0.2): + def __init__(self, linear_width, numticks=11, symthresh=0.2, + base=0, subs=None): """ Parameters ---------- @@ -2611,6 +2612,8 @@ def __init__(self, linear_width, numticks=11, symthresh=0.2): self.linear_width = linear_width self.numticks = numticks self.symthresh = symthresh + self.base = base + self.subs = subs def set_params(self, numticks=None, symthresh=None): """Set parameters within this locator.""" @@ -2642,19 +2645,33 @@ def tick_values(self, vmin, vmax): # Transform the "on-screen" grid to the data space: xs = self.linear_width * np.sinh(ys / self.linear_width) - zero_xs = (xs == 0) - - # Round the data-space values to be intuitive decimal numbers: - decades = ( - np.where(xs >= 0, 1, -1) * - np.power(10, np.where(zero_xs, 0.0, - np.floor(np.log10(np.abs(xs) - + zero_xs*1e-6)))) - ) - qs = decades * np.round(xs / decades) + zero_xs = (ys == 0) + + # Round the data-space values to be intuitive base-n numbers: + if self.base > 1: + log_base = math.log(self.base) + powers = ( + np.where(zero_xs, 0, np.where(xs >=0, 1, -1)) * + np.power(self.base, + np.where(zero_xs, 0.0, + np.floor(np.log(np.abs(xs) + zero_xs*1e-6) + / log_base))) + ) + if self.subs: + qs = np.outer(powers, self.subs).flatten() + else: + qs = powers + else: + powers = ( + np.where(xs >= 0, 1, -1) * + np.power(10, np.where(zero_xs, 0.0, + np.floor(np.log10(np.abs(xs) + + zero_xs*1e-6)))) + ) + qs = powers * np.round(xs / powers) ticks = np.array(sorted(set(qs))) - if len(ticks) > self.numticks // 2: + if len(ticks) >= 2: return ticks else: return np.linspace(vmin, vmax, self.numticks) From 8168e56ac2fd8b619aceaa72f605562c7be1fe65 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sat, 16 Oct 2021 10:44:16 +0100 Subject: [PATCH 19/36] Improved minor-tick location for common number bases and widened test coverage --- examples/scales/asinh_demo.py | 4 +-- lib/matplotlib/scale.py | 26 +++++++++++++++++-- lib/matplotlib/tests/test_scale.py | 13 ++++++++++ lib/matplotlib/tests/test_ticker.py | 40 +++++++++++++++++++++++++---- lib/matplotlib/ticker.py | 15 ++++++++++- 5 files changed, 88 insertions(+), 10 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 531b58445e55..bf95e4d4aa8e 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -64,12 +64,12 @@ # 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 in zip(axs, (0.2, 1.0, 5.0)): +for ax, (a0, base) in zip(axs, ((0.2, 2), (1.0, 0), (5.0, 3))): 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) + ax.set_yscale('asinh', linear_width=a0, base=base) ax.grid() ax.legend(loc='best', fontsize='small') diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 1462ef6f370b..0ebfb0154ee4 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -511,8 +511,18 @@ class AsinhScale(ScaleBase): name = 'asinh' + auto_tick_multipliers = { + 3: (2, ), + 4: (2, ), + 5: (2, ), + 8: (2, 4), + 10: (2, 5), + 16: (2, 4, 8), + 64: (4, 16), + 1024: (256, 512) } + def __init__(self, axis, *, linear_width=1.0, - base=10, subs=(2, 5), **kwargs): + base=10, subs='auto', **kwargs): """ Parameters ---------- @@ -521,11 +531,23 @@ def __init__(self, axis, *, linear_width=1.0, defining the extent of the quasi-linear region, and the coordinate values beyond which the transformation becomes asympotically logarithmic. + base : int, default: 10 + The number base used for rounding tick locations + on a logarithmic scale. If this is less than one, + then rounding is to the nearest integer multiple + of powers of ten. + subs : sequence of int + Multiples of the number base used for minor ticks. + If set to 'auto', this will use built-in defaults, + e.g. (2, 5) for base=10. """ super().__init__(axis) self._transform = AsinhTransform(linear_width) self._base = int(base) - self._subs = subs + if subs == 'auto': + self._subs = self.auto_tick_multipliers.get(self._base) + else: + self._subs = subs linear_width = property(lambda self: self._transform.linear_width) diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 51da63da80a7..f175187ce305 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -243,11 +243,24 @@ def test_init(self): s = AsinhScale(axis=None, linear_width=23.0) assert s.linear_width == 23 + assert s._base == 10 + assert s._subs == (2, 5) tx = s.get_transform() assert isinstance(tx, AsinhTransform) assert tx.linear_width == s.linear_width + def test_base_init(self): + fig, ax = plt.subplots() + + s3 = AsinhScale(axis=None, base=3) + assert s3._base == 3 + assert s3._subs == (2,) + + s7 = AsinhScale(axis=None, base=7) + assert s7._base == 7 + assert s7._subs == (2, 5) + def test_bad_scale(self): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index ef42f87624d7..1e623fc5f1cc 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -448,12 +448,16 @@ def test_init(self): lctr = mticker.AsinhLocator(linear_width=2.718, numticks=19) assert lctr.linear_width == 2.718 assert lctr.numticks == 19 + assert lctr.base == 0 def test_set_params(self): lctr = mticker.AsinhLocator(linear_width=5, - numticks=17, symthresh=0.125) + numticks=17, symthresh=0.125, + base=4, subs=(2.5, 3.25)) assert lctr.numticks == 17 assert lctr.symthresh == 0.125 + assert lctr.base == 4 + assert lctr.subs == (2.5, 3.25) lctr.set_params(numticks=23) assert lctr.numticks == 23 @@ -465,8 +469,20 @@ def test_set_params(self): lctr.set_params(symthresh=None) assert lctr.symthresh == 0.5 + lctr.set_params(base=7) + assert lctr.base == 7 + lctr.set_params(base=None) + assert lctr.base == 7 + + lctr.set_params(subs=(2, 4.125)) + assert lctr.subs == (2, 4.125) + lctr.set_params(subs=None) + assert lctr.subs == (2, 4.125) + lctr.set_params(subs=[]) + assert lctr.subs is None + def test_linear_values(self): - lctr = mticker.AsinhLocator(linear_width=100, numticks=11) + lctr = mticker.AsinhLocator(linear_width=100, numticks=11, base=0) assert_almost_equal(lctr.tick_values(-1, 1), np.arange(-1, 1.01, 0.2)) @@ -476,7 +492,7 @@ def test_linear_values(self): np.arange(-0.01, 0.0101, 0.002)) def test_wide_values(self): - lctr = mticker.AsinhLocator(linear_width=0.1, numticks=11) + lctr = mticker.AsinhLocator(linear_width=0.1, numticks=11, base=0) assert_almost_equal(lctr.tick_values(-100, 100), [-100, -20, -5, -1, -0.2, @@ -487,7 +503,7 @@ def test_wide_values(self): def test_near_zero(self): """Check that manually injected zero will supersede nearby tick""" - lctr = mticker.AsinhLocator(linear_width=100, numticks=3) + lctr = mticker.AsinhLocator(linear_width=100, numticks=3, base=0) assert_almost_equal(lctr.tick_values(-1.1, 0.9), [-1.0, 0.0, 0.9]) @@ -504,7 +520,7 @@ class DummyAxis: def get_data_interval(cls): return cls.bounds lctr = mticker.AsinhLocator(linear_width=1, numticks=3, - symthresh=0.25) + symthresh=0.25, base=0) lctr.axis = DummyAxis DummyAxis.bounds = (-1, 2) @@ -519,6 +535,20 @@ def get_data_interval(cls): return cls.bounds DummyAxis.bounds = (1, 1.1) assert_almost_equal(lctr(), [1, 1.05, 1.1]) + def test_base_rounding(self): + lctr10 = mticker.AsinhLocator(linear_width=1, numticks=8, + base=10, subs=(1, 3, 5)) + assert_almost_equal(lctr10.tick_values(-110, 110), + [-500, -300, -100, -50, -30, -10, -5, -3, -1, + -0.5, -0.3, -0.1, 0, 0.1, 0.3, 0.5, + 1, 3, 5, 10, 30, 50, 100, 300, 500]) + + lctr5 = mticker.AsinhLocator(linear_width=1, numticks=20, base=5) + assert_almost_equal(lctr5.tick_values(-1050, 1050), + [-625, -125, -25, -5, -1, -0.2, 0, + 0.2, 1, 5, 25, 125, 625]) + + class TestScalarFormatter: offset_data = [ diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index a0527c4d3fa3..a193f9e3980e 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2607,6 +2607,14 @@ def __init__(self, linear_width, numticks=11, symthresh=0.2, The fractional threshold beneath which data which covers a range that is approximately symmetric about zero will have ticks that are exactly symmetric. + base : int, default: 0 + The number base used for rounding tick locations + on a logarithmic scale. If this is less than one, + then rounding is to the nearest integer multiple + of powers of ten. + subs : tuple, default: None + Multiples of the number base, typically used + for the minor ticks, e.g. (2, 5) when base=10. """ super().__init__() self.linear_width = linear_width @@ -2615,12 +2623,17 @@ def __init__(self, linear_width, numticks=11, symthresh=0.2, self.base = base self.subs = subs - def set_params(self, numticks=None, symthresh=None): + def set_params(self, numticks=None, symthresh=None, + base=None, subs=None): """Set parameters within this locator.""" if numticks is not None: self.numticks = numticks if symthresh is not None: self.symthresh = symthresh + if base is not None: + self.base = base + if subs is not None: + self.subs = subs if len(subs) > 0 else None def __call__(self): dmin, dmax = self.axis.get_data_interval() From 024986119bde488dc3454a2716faa0a0bbc9b6ec Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sat, 16 Oct 2021 12:11:19 +0100 Subject: [PATCH 20/36] Added cross-references between symlog and asinh demo pages --- examples/scales/asinh_demo.py | 9 ++++++++- examples/scales/symlog_demo.py | 14 ++++++++++++++ lib/matplotlib/tests/test_scale.py | 4 ++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index bf95e4d4aa8e..a8ec487f0c2b 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -95,5 +95,12 @@ ax.set_ylim(-50, 50) ax.grid() - plt.show() + +######################################## +# +# .. admonition:: References +# +# - `matplotlib.scale.AsinhScale` +# - `matplotlib.ticker.AsinhLocator` +# - `matplotlib.scale.SymmetricalLogScale` diff --git a/examples/scales/symlog_demo.py b/examples/scales/symlog_demo.py index e1c433b22b88..1aa18e8fbcf0 100644 --- a/examples/scales/symlog_demo.py +++ b/examples/scales/symlog_demo.py @@ -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` diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index f175187ce305..cfb82b963620 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -257,9 +257,9 @@ def test_base_init(self): assert s3._base == 3 assert s3._subs == (2,) - s7 = AsinhScale(axis=None, base=7) + s7 = AsinhScale(axis=None, base=7, subs=(2, 4)) assert s7._base == 7 - assert s7._subs == (2, 5) + assert s7._subs == (2, 4) def test_bad_scale(self): fig, ax = plt.subplots() From 9fd8b0cf8c3066a8aa7271ca4cc37eae939e02da Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sat, 16 Oct 2021 13:08:33 +0100 Subject: [PATCH 21/36] Added AsinhNorm for colorscale support --- .../colormap_normalizations_symlognorm.py | 2 +- lib/matplotlib/colors.py | 21 ++++++++++++++++++ lib/matplotlib/scale.py | 3 ++- lib/matplotlib/tests/test_colors.py | 22 +++++++++++++++++++ lib/matplotlib/tests/test_ticker.py | 1 - lib/matplotlib/ticker.py | 4 ++-- 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py index 7856068fd1d8..caa08d1a8303 100644 --- a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -25,7 +25,7 @@ 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 +Z = (5 * Z1 - Z2) * 2 fig, ax = plt.subplots(2, 1) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index b5f66bc33224..16d866b67500 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1652,6 +1652,27 @@ 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. + """ + + @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 diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 0ebfb0154ee4..e1f86d8b414a 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -519,7 +519,8 @@ class AsinhScale(ScaleBase): 10: (2, 5), 16: (2, 4, 8), 64: (4, 16), - 1024: (256, 512) } + 1024: (256, 512) + } def __init__(self, axis, *, linear_width=1.0, base=10, subs='auto', **kwargs): diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 4fa65918e7fa..79b33a8d86c3 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -745,6 +745,28 @@ def test_SymLogNorm_single_zero(): plt.close(fig) +class TestAsinhNorm: + """ + Tests for `~.colors.AsinhNorm` + """ + + def test_init(self): + norm0 = mcolors.AsinhNorm() + assert norm0.linear_width == 1 + + norm5 = mcolors.AsinhNorm(linear_width=5) + assert norm5.linear_width == 5 + + def test_norm(self): + norm = mcolors.AsinhNorm(2, vmin=-4, vmax=4) + vals = np.arange(-3.5, 3.5, 10) + normed_vals = norm(vals) + asinh2 = np.arcsinh(2) + + expected = (2 * np.arcsinh(vals / 2) + 2 * asinh2) / (4 * asinh2) + assert_array_almost_equal(normed_vals, expected) + + def _inverse_tester(norm_instance, vals): """ Checks if the inverse of the given normalization is working. diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 1e623fc5f1cc..ce4bba3100a5 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -549,7 +549,6 @@ def test_base_rounding(self): 0.2, 1, 5, 25, 125, 625]) - class TestScalarFormatter: offset_data = [ (123, 189, 0), diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index a193f9e3980e..3669e99ceb51 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2664,7 +2664,7 @@ def tick_values(self, vmin, vmax): if self.base > 1: log_base = math.log(self.base) powers = ( - np.where(zero_xs, 0, np.where(xs >=0, 1, -1)) * + np.where(zero_xs, 0, np.where(xs >= 0, 1, -1)) * np.power(self.base, np.where(zero_xs, 0.0, np.floor(np.log(np.abs(xs) + zero_xs*1e-6) @@ -2684,7 +2684,7 @@ def tick_values(self, vmin, vmax): qs = powers * np.round(xs / powers) ticks = np.array(sorted(set(qs))) - if len(ticks) >= 2: + if len(ticks) >= 2: return ticks else: return np.linspace(vmin, vmax, self.numticks) From 8d7c2adfa6ee45ecebac987b2945f6bb19104275 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sat, 16 Oct 2021 13:33:17 +0100 Subject: [PATCH 22/36] Fixed SymlogNorm demo to actually use positive & negative values --- .../colormap_normalizations_symlognorm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py index caa08d1a8303..cc368927960e 100644 --- a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -25,17 +25,17 @@ 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 = (5 * Z1 - Z2) * 2 +Z = 5 * Z1 - Z2 fig, ax = plt.subplots(2, 1) pcm = ax[0].pcolormesh(X, Y, Z, - norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03, - vmin=-1.0, vmax=1.0, base=10), + norm=colors.SymLogNorm(linthresh=0.5, linscale=1, + vmin=-5, vmax=5, base=10), cmap='RdBu_r', shading='nearest') fig.colorbar(pcm, ax=ax[0], extend='both') -pcm = ax[1].pcolormesh(X, Y, Z, cmap='RdBu_r', vmin=-np.max(Z), +pcm = ax[1].pcolormesh(X, Y, Z, cmap='RdBu_r', vmin=-5, vmax=5, shading='nearest') fig.colorbar(pcm, ax=ax[1], extend='both') From ae57d8d8800ecf985e5179a15276c32d4fb39963 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sat, 16 Oct 2021 15:34:10 +0100 Subject: [PATCH 23/36] Further refinements to documentation and test coverage --- doc/api/colors_api.rst | 1 + lib/matplotlib/colors.py | 6 ++++++ lib/matplotlib/tests/test_scale.py | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/doc/api/colors_api.rst b/doc/api/colors_api.rst index e7b6da70f641..44f8cca303fd 100644 --- a/doc/api/colors_api.rst +++ b/doc/api/colors_api.rst @@ -21,6 +21,7 @@ Classes :toctree: _as_gen/ :template: autosummary.rst + AsinhNorm BoundaryNorm Colormap CenteredNorm diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 16d866b67500..5b8e2143a336 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1662,6 +1662,12 @@ class AsinhNorm(Normalize): or negative values. Unlike the `SymLogNorm`, the transition between these linear and logarithmic regions is smooth, which may reduce the risk of visual artifacts. + + Parameters + ---------- + linear_width : float, default: 1 + The effective width of the linear region, beyond which + the transformation becomes asymptotically logarithmic """ @property diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index cfb82b963620..7f1130560581 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -6,6 +6,7 @@ LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale +from matplotlib.ticker import AsinhLocator, LogFormatterSciNotation from matplotlib.testing.decorators import check_figures_equal, image_comparison import numpy as np @@ -261,6 +262,28 @@ def test_base_init(self): assert s7._base == 7 assert s7._subs == (2, 4) + def test_fmtloc(self): + class DummyAxis: + def __init__(self): + self.fields = {} + def set(self, **kwargs): + self.fields.update(**kwargs) + def set_major_formatter(self, f): + self.fields['major_formatter'] = f + + ax0 = DummyAxis() + s0 = AsinhScale(axis=ax0, base=0) + s0.set_default_locators_and_formatters(ax0) + assert isinstance(ax0.fields['major_locator'], AsinhLocator) + assert isinstance(ax0.fields['major_formatter'], str) + + ax5 = DummyAxis() + s7 = AsinhScale(axis=ax5, base=5) + s7.set_default_locators_and_formatters(ax5) + assert isinstance(ax5.fields['major_locator'], AsinhLocator) + assert isinstance(ax5.fields['major_formatter'], + LogFormatterSciNotation) + def test_bad_scale(self): fig, ax = plt.subplots() From b01d03c2b7dc5fc43a0ed54baa0cd4ce29a24636 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 17 Oct 2021 08:31:45 +0100 Subject: [PATCH 24/36] Reworked SymLogNorm demonstration, and added comparision with AsinhNorm --- .../colormap_normalizations_symlognorm.py | 78 ++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py index cc368927960e..822a873a0bbb 100644 --- a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -1,6 +1,6 @@ """ ================================== -Colormap Normalizations Symlognorm +Colormap Normalizations SymLogNorm ================================== Demonstration of using norm to map colormaps onto data in non-linear ways. @@ -8,35 +8,75 @@ .. 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. +N = 200 +gain = 8 +X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)] +rbf = lambda x, y: 1.0 / (1 + 5 * ((x ** 2) + (y ** 2))) +Z1 = rbf(X + 0.5, Y + 0.5) +Z2 = rbf(X - 0.5, Y - 0.5) +Z = gain * Z1 - Z2 -Note that colorbar labels do not come out looking very good. -""" +shadeopts = { 'cmap': 'PRGn', 'shading': 'gouraud' } +colormap = 'PRGn' +lnrwidth = 0.2 -N = 100 -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 = 5 * Z1 - Z2 +fig, ax = plt.subplots(2, 1, sharex=True, sharey=True) -fig, ax = plt.subplots(2, 1) +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, vmin=-gain, vmax=gain, + **shadeopts) +fig.colorbar(pcm, ax=ax[1], extend='both') +ax[1].text(-2.5, 1.5, 'linear') + + +######################################## +# Clearly, tt may be necessary to experiment with multiple different +# colorscales in order to find the best visualization for +# any particular dataset. +# As well as the `~.colors.SymLogNorm` scaling, there is also +# the option of using the `~.colors.AsinhNorm`, which has a smoother +# transition between the linear and logarithmic regions of the transformation +# applied to the "z" axis. +# In the plots below, it may be possible to see ring-like artifacts +# in the lower-amplitude, negative, hump shown in purple 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=0.5, linscale=1, - vmin=-5, vmax=5, 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=-5, vmax=5, - shading='nearest') +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() From e0dcff7ff5ad7f9d39efec900ab3af06895f22f4 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 17 Oct 2021 10:54:42 +0100 Subject: [PATCH 25/36] Tweaked flake8 issues --- .../colormap_normalizations_symlognorm.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py index 822a873a0bbb..d2418cc9ed43 100644 --- a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -22,15 +22,18 @@ import matplotlib.pyplot as plt import matplotlib.colors as colors + +def rbf(x, y): + return 1.0 / (1 + 5 * ((x ** 2) + (y ** 2))) + N = 200 gain = 8 X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)] -rbf = lambda x, y: 1.0 / (1 + 5 * ((x ** 2) + (y ** 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' } +shadeopts = {'cmap': 'PRGn', 'shading': 'gouraud'} colormap = 'PRGn' lnrwidth = 0.2 @@ -66,14 +69,14 @@ pcm = ax[0].pcolormesh(X, Y, Z, norm=colors.SymLogNorm(linthresh=lnrwidth, linscale=1, - vmin=-gain, vmax=gain, base=10), + vmin=-2, 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), + vmin=-2, vmax=gain), **shadeopts) fig.colorbar(pcm, ax=ax[1], extend='both') ax[1].text(-2.5, 1.5, 'asinh') From 3d92406ae4b343783611d668296c6626ed7adfdc Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Tue, 26 Oct 2021 17:35:17 +0100 Subject: [PATCH 26/36] Apply suggestions from code review by greglucas Co-authored-by: Greg Lucas --- doc/users/next_whats_new/asinh_scale.rst | 4 ++-- examples/scales/asinh_demo.py | 4 ++-- lib/matplotlib/scale.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/users/next_whats_new/asinh_scale.rst b/doc/users/next_whats_new/asinh_scale.rst index fa48b4117182..cec844762905 100644 --- a/doc/users/next_whats_new/asinh_scale.rst +++ b/doc/users/next_whats_new/asinh_scale.rst @@ -4,13 +4,13 @@ New axis scale ``asinh`` 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 than span many orders +allows plotting both positive and negative values that span many orders of magnitude. A scale parameter is provided to allow the user to tune the width of the linear region of the scale. .. plot:: - from matplotlib import pyplot as plt + import matplotlib.pyplot as plt import numpy fig, (ax0, ax1) = plt.subplots(1, 2, sharex=True) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index a8ec487f0c2b..1b441eea300d 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -31,7 +31,7 @@ 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 should reduce visual artifacts associated with an abrupt +and reduces visual artifacts associated with an abrupt transition between linear and logarithmic regions of the plot. See `~.scale.AsinhScale`, `~.scale.SymmetricalLogScale`. @@ -45,7 +45,7 @@ ######################################## # Compare "symlog" and "asinh" behaviour on sample y=x graph, -# where the discontinuous gradient in "symlog" near y=2 is obvious: +# where there is a discontinuous gradient in "symlog" near y=2: fig1 = plt.figure() ax0, ax1 = fig1.subplots(1, 2, sharex=True) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index e1f86d8b414a..b2a13801f942 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -465,7 +465,7 @@ class AsinhTransform(Transform): def __init__(self, linear_width): super().__init__() if linear_width <= 0.0: - raise ValueError("Scale parameter 'a0' must be strictly positive") + raise ValueError("Scale parameter 'linear_width' must be strictly positive") self.linear_width = linear_width def transform_non_affine(self, a): From 7bec3f478da7c6eee5d7e849175e229c0e1ace67 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Tue, 26 Oct 2021 18:58:57 +0100 Subject: [PATCH 27/36] Patched tick-generation on pan/zoom & misc. tidying --- doc/users/next_whats_new/asinh_scale.rst | 3 +-- .../colormap_normalizations_symlognorm.py | 14 +++++++------- examples/scales/asinh_demo.py | 2 +- lib/matplotlib/scale.py | 3 ++- lib/matplotlib/tests/test_ticker.py | 2 +- lib/matplotlib/ticker.py | 12 +++++++----- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/doc/users/next_whats_new/asinh_scale.rst b/doc/users/next_whats_new/asinh_scale.rst index cec844762905..c70778d8ac94 100644 --- a/doc/users/next_whats_new/asinh_scale.rst +++ b/doc/users/next_whats_new/asinh_scale.rst @@ -5,8 +5,7 @@ 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. A scale parameter is provided to allow the user -to tune the width of the linear region of the scale. +of magnitude. .. plot:: diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py index d2418cc9ed43..a9ca69d84d56 100644 --- a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -35,7 +35,7 @@ def rbf(x, y): shadeopts = {'cmap': 'PRGn', 'shading': 'gouraud'} colormap = 'PRGn' -lnrwidth = 0.2 +lnrwidth = 0.5 fig, ax = plt.subplots(2, 1, sharex=True, sharey=True) @@ -60,23 +60,23 @@ def rbf(x, y): # the option of using the `~.colors.AsinhNorm`, which has a smoother # transition between the linear and logarithmic regions of the transformation # applied to the "z" axis. -# In the plots below, it may be possible to see ring-like artifacts -# in the lower-amplitude, negative, hump shown in purple despite -# there being no sharp features in the dataset itself. -# The ``asinh`` scaling shows a smoother shading of each hump. +# 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=-2, vmax=gain, base=10), + 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=-2, vmax=gain), + vmin=-gain, vmax=gain), **shadeopts) fig.colorbar(pcm, ax=ax[1], extend='both') ax[1].text(-2.5, 1.5, 'asinh') diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 1b441eea300d..074a7fc6a403 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -64,7 +64,7 @@ # 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, 3))): +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') diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index b2a13801f942..d7137b06ae8b 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -465,7 +465,8 @@ class AsinhTransform(Transform): def __init__(self, linear_width): super().__init__() if linear_width <= 0.0: - raise ValueError("Scale parameter 'linear_width' must be strictly positive") + raise ValueError("Scale parameter 'linear_width' " + + "must be strictly positive") self.linear_width = linear_width def transform_non_affine(self, a): diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index ce4bba3100a5..6411d803bfba 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -517,7 +517,7 @@ def test_symmetrizing(self): class DummyAxis: bounds = (-1, 1) @classmethod - def get_data_interval(cls): return cls.bounds + def get_view_interval(cls): return cls.bounds lctr = mticker.AsinhLocator(linear_width=1, numticks=3, symthresh=0.25, base=0) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 3669e99ceb51..5badae1fca02 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2636,13 +2636,13 @@ def set_params(self, numticks=None, symthresh=None, self.subs = subs if len(subs) > 0 else None def __call__(self): - dmin, dmax = self.axis.get_data_interval() - if (dmin * dmax) < 0 and abs(1 + dmax / dmin) < self.symthresh: + vmin, vmax = self.axis.get_view_interval() + if (vmin * vmax) < 0 and abs(1 + vmax / vmin) < self.symthresh: # Data-range appears to be almost symmetric, so round up: - bound = max(abs(dmin), abs(dmax)) + bound = max(abs(vmin), abs(vmax)) return self.tick_values(-bound, bound) else: - return self.tick_values(dmin, dmax) + return self.tick_values(vmin, vmax) def tick_values(self, vmin, vmax): # Construct a set of "on-screen" locations @@ -2660,7 +2660,9 @@ def tick_values(self, vmin, vmax): xs = self.linear_width * np.sinh(ys / self.linear_width) zero_xs = (ys == 0) - # Round the data-space values to be intuitive base-n numbers: + # Round the data-space values to be intuitive base-n numbers, + # keeping track of positive and negative values separately, + # but giving careful treatment to the zero value: if self.base > 1: log_base = math.log(self.base) powers = ( From 97693e8d906816c1941eecad447809aaea759622 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Tue, 26 Oct 2021 19:09:44 +0100 Subject: [PATCH 28/36] Patched overzealous zero edge-case in ticker --- lib/matplotlib/ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 5badae1fca02..2bebbbe640d9 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2666,7 +2666,7 @@ def tick_values(self, vmin, vmax): if self.base > 1: log_base = math.log(self.base) powers = ( - np.where(zero_xs, 0, np.where(xs >= 0, 1, -1)) * + np.where(zero_xs, 0, np.sign(xs)) * np.power(self.base, np.where(zero_xs, 0.0, np.floor(np.log(np.abs(xs) + zero_xs*1e-6) From a1af12c91aa948795d12b63cb2d1a60f72544e37 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sun, 31 Oct 2021 16:07:03 +0000 Subject: [PATCH 29/36] Apply suggestions from code review Co-authored-by: Greg Lucas --- .../colormap_normalizations_symlognorm.py | 7 +++---- lib/matplotlib/scale.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py index a9ca69d84d56..66dc6e467a6b 100644 --- a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -53,13 +53,12 @@ def rbf(x, y): ######################################## -# Clearly, tt may be necessary to experiment with multiple different -# colorscales in order to find the best visualization for -# any particular dataset. +# 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 the `~.colors.AsinhNorm`, which has a smoother # transition between the linear and logarithmic regions of the transformation -# applied to the "z" axis. +# 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 diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index d7137b06ae8b..eceabf2c8498 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -477,7 +477,7 @@ def inverted(self): class InvertedAsinhTransform(Transform): - """Hyperbolic-sine transformation used by `.AsinhScale`""" + """Hyperbolic sine transformation used by `.AsinhScale`""" input_dims = output_dims = 1 def __init__(self, linear_width): @@ -496,7 +496,7 @@ class AsinhScale(ScaleBase): A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh) For values close to zero, this is essentially a linear scale, - but for larger values (either positive or negative) it is asymptotically + but for large magnitude values (either positive or negative) it is asymptotically logarithmic. The transition between these linear and logarithmic regimes is smooth, and has no discontinuities in the function gradient in contrast to the `.SymmetricalLogScale` ("symlog") scale. From 555bac1554d7984a8dfc2c49161fdba971211dc8 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sat, 20 Nov 2021 10:17:13 +0000 Subject: [PATCH 30/36] Patched unit-test for default base in AsinhLocator --- lib/matplotlib/tests/test_ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 6411d803bfba..1ff2e1da5c85 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -448,7 +448,7 @@ def test_init(self): lctr = mticker.AsinhLocator(linear_width=2.718, numticks=19) assert lctr.linear_width == 2.718 assert lctr.numticks == 19 - assert lctr.base == 0 + assert lctr.base == 10 def test_set_params(self): lctr = mticker.AsinhLocator(linear_width=5, From a2b210d77d48dcbceb395714b6d0f80ebe1da6aa Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Wed, 2 Feb 2022 17:02:09 +0000 Subject: [PATCH 31/36] Added documentation comments forewarning of possible API changes --- lib/matplotlib/colors.py | 5 +++++ lib/matplotlib/scale.py | 5 +++++ lib/matplotlib/ticker.py | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 45c49ac609a7..8461411741eb 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1691,6 +1691,11 @@ class AsinhNorm(Normalize): 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 diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index b7d3cfce8ccd..51fe384f993e 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -508,6 +508,11 @@ class AsinhScale(ScaleBase): :math:`a \\rightarrow a + {\\cal O}(a^3)`. For large values of :math:`a` the transformation behaves as :math:`a \\rightarrow a_0 \\, {\\rm sgn}(a) \\ln |a| + {\\cal O}(1)`. + + .. note:: + + This API is provisional and may be revised in the future + based on early user feedback. """ name = 'asinh' diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 292baaad06a5..ff2e4153cbf5 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2613,6 +2613,11 @@ class AsinhLocator(Locator): This is very unlikely to have any use beyond the `~.scale.AsinhScale` class. + + .. note:: + + This API is provisional and may be revised in the future + based on early user feedback. """ def __init__(self, linear_width, numticks=11, symthresh=0.2, base=10, subs=None): From 8a258cf92db5b3e209018fe3775702ad2cd50ef1 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Thu, 3 Feb 2022 16:31:50 +0000 Subject: [PATCH 32/36] Apply suggestions from code review Co-authored-by: Jody Klymak --- doc/users/next_whats_new/asinh_scale.rst | 4 ++-- .../colormap_normalizations_symlognorm.py | 2 +- examples/scales/asinh_demo.py | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/users/next_whats_new/asinh_scale.rst b/doc/users/next_whats_new/asinh_scale.rst index c70778d8ac94..c2c5c89e6ade 100644 --- a/doc/users/next_whats_new/asinh_scale.rst +++ b/doc/users/next_whats_new/asinh_scale.rst @@ -1,5 +1,5 @@ -New axis scale ``asinh`` ------------------------- +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 diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py index 66dc6e467a6b..b60ec4b59f29 100644 --- a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -56,7 +56,7 @@ def rbf(x, y): # 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 the `~.colors.AsinhNorm`, which has a smoother +# 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 diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 074a7fc6a403..8474dfcb68e1 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -34,6 +34,8 @@ 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`. """ From 5a03297072a62f479196ccd79ed08288425402c3 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Thu, 3 Feb 2022 16:33:37 +0000 Subject: [PATCH 33/36] Update examples/scales/asinh_demo.py Co-authored-by: Jody Klymak --- examples/scales/asinh_demo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 8474dfcb68e1..1117d6998565 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -34,6 +34,8 @@ 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. .. note:: `.scale.AsinhScale` is experimental, and the API may change. See `~.scale.AsinhScale`, `~.scale.SymmetricalLogScale`. From de305381ea100cbfab976f764c0161db44cbe396 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Thu, 3 Feb 2022 16:37:34 +0000 Subject: [PATCH 34/36] Minor repair of duplicate "experimental" annotation --- examples/scales/asinh_demo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index 1117d6998565..a9869d0629d3 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -36,8 +36,7 @@ .. note:: `.scale.AsinhScale` is experimental, and the API may change. -.. note:: - `.scale.AsinhScale` is experimental, and the API may change. + See `~.scale.AsinhScale`, `~.scale.SymmetricalLogScale`. """ From c64d0c4d975337fcbdfdcfd4209f86a61ab86411 Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Fri, 11 Feb 2022 17:07:08 +0000 Subject: [PATCH 35/36] Tweaked indendation etc. following review by @QuLogic --- doc/users/next_whats_new/asinh_scale.rst | 12 ++++++------ .../colormap_normalizations_symlognorm.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/users/next_whats_new/asinh_scale.rst b/doc/users/next_whats_new/asinh_scale.rst index c2c5c89e6ade..69238ebe220a 100644 --- a/doc/users/next_whats_new/asinh_scale.rst +++ b/doc/users/next_whats_new/asinh_scale.rst @@ -10,10 +10,10 @@ of magnitude. .. plot:: import matplotlib.pyplot as plt - import numpy + import numpy as np fig, (ax0, ax1) = plt.subplots(1, 2, sharex=True) - x = numpy.linspace(-3, 6, 100) + x = np.linspace(-3, 6, 100) ax0.plot(x, x) ax0.set_yscale('symlog') @@ -26,7 +26,7 @@ of magnitude. 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) + 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) diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py index b60ec4b59f29..42a3878143d3 100644 --- a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -8,7 +8,7 @@ .. 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, @@ -52,7 +52,7 @@ def rbf(x, y): 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 From e43cbfda852efaeeb3882130aff96fcb9bef7b7d Mon Sep 17 00:00:00 2001 From: Richard Penney Date: Sat, 12 Feb 2022 08:30:51 +0000 Subject: [PATCH 36/36] Patched various 79-character section breaks in documentation --- examples/scales/asinh_demo.py | 8 ++++---- examples/scales/symlog_demo.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py index a9869d0629d3..f1ee691a89e7 100644 --- a/examples/scales/asinh_demo.py +++ b/examples/scales/asinh_demo.py @@ -46,7 +46,7 @@ # 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() @@ -63,7 +63,7 @@ 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) @@ -77,7 +77,7 @@ 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 @@ -100,7 +100,7 @@ plt.show() -######################################## +############################################################################### # # .. admonition:: References # diff --git a/examples/scales/symlog_demo.py b/examples/scales/symlog_demo.py index 1aa18e8fbcf0..e9cdfff5355e 100644 --- a/examples/scales/symlog_demo.py +++ b/examples/scales/symlog_demo.py @@ -34,13 +34,13 @@ 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 #