diff --git a/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst b/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst index cd7dee07f459..b98efee8e6b6 100644 --- a/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst +++ b/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst @@ -307,3 +307,15 @@ mplot3d auto-registration longer necessary to import mplot3d to create 3d axes with :: ax = fig.add_subplot(111, projection="3d") + +`.SymLogNorm` now has a *base* parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, `.SymLogNorm` had no *base* keyword argument and the base was +hard-coded to ``base=np.e``. This was inconsistent with the default +behavior of `.SymLogScale` (which defaults to ``base=10``) and the use +of the word "decade" in the documentation. + +In preparation for changing the default base to 10, calling +`.SymLogNorm` without the new *base* kwarg emits a deprecation +warning. diff --git a/examples/userdemo/colormap_normalizations.py b/examples/userdemo/colormap_normalizations.py index b13d7f213cf5..c3614efb943a 100644 --- a/examples/userdemo/colormap_normalizations.py +++ b/examples/userdemo/colormap_normalizations.py @@ -69,7 +69,7 @@ pcm = ax[0].pcolormesh(X, Y, Z1, norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03, - vmin=-1.0, vmax=1.0), + vmin=-1.0, vmax=1.0, base=10), cmap='RdBu_r') fig.colorbar(pcm, ax=ax[0], extend='both') diff --git a/examples/userdemo/colormap_normalizations_symlognorm.py b/examples/userdemo/colormap_normalizations_symlognorm.py index 780381e43da8..b0fbf0dc30ea 100644 --- a/examples/userdemo/colormap_normalizations_symlognorm.py +++ b/examples/userdemo/colormap_normalizations_symlognorm.py @@ -29,7 +29,7 @@ pcm = ax[0].pcolormesh(X, Y, Z, norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03, - vmin=-1.0, vmax=1.0), + vmin=-1.0, vmax=1.0, base=10), cmap='RdBu_r') fig.colorbar(pcm, ax=ax[0], extend='both') diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index b58bfc57a944..9614b0360f75 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1209,25 +1209,47 @@ class SymLogNorm(Normalize): *linthresh* allows the user to specify the size of this range (-*linthresh*, *linthresh*). """ - def __init__(self, linthresh, linscale=1.0, - vmin=None, vmax=None, clip=False): + def __init__(self, linthresh, linscale=1.0, vmin=None, vmax=None, + clip=False, *, base=None): """ - *linthresh*: - The range within which the plot is linear (to - avoid having the plot go to infinity around zero). - - *linscale*: - This allows the linear range (-*linthresh* to *linthresh*) - to be stretched relative to the logarithmic range. Its - value is the number of decades to use for each half of the - linear range. For example, when *linscale* == 1.0 (the - default), the space used for the positive and negative - halves of the linear range will be equal to one decade in - the logarithmic range. Defaults to 1. + Parameters + ---------- + linthresh : float + The range within which the plot is linear (to avoid having the plot + go to infinity around zero). + linscale : float, default: 1 + This allows the linear range (-*linthresh* to *linthresh*) + to be stretched relative to the logarithmic range. Its + value is the number of powers of *base* to use for each + half of the linear range. + + For example, when *linscale* == 1.0 (the default) and + ``base=10``, then space used for the positive and negative + halves of the linear range will be equal to a decade in + the logarithmic. + + base : float, default: None + If not given, defaults to ``np.e`` (consistent with prior + behavior) and warns. + + In v3.3 the default value will change to 10 to be consistent with + `.SymLogNorm`. + + To suppress the warning pass *base* as a keyword argument. + """ Normalize.__init__(self, vmin, vmax, clip) + if base is None: + self._base = np.e + cbook.warn_deprecated("3.3", message="default base may change " + "from np.e to 10. To suppress this warning specify the base " + "keyword argument.") + else: + self._base = base + self._log_base = np.log(self._base) + self.linthresh = float(linthresh) - self._linscale_adj = (linscale / (1.0 - np.e ** -1)) + self._linscale_adj = (linscale / (1.0 - self._base ** -1)) if vmin is not None and vmax is not None: self._transform_vmin_vmax() @@ -1262,7 +1284,8 @@ def _transform(self, a): with np.errstate(invalid="ignore"): masked = np.abs(a) > self.linthresh sign = np.sign(a[masked]) - log = (self._linscale_adj + np.log(np.abs(a[masked]) / self.linthresh)) + log = (self._linscale_adj + + np.log(np.abs(a[masked]) / self.linthresh) / self._log_base) log *= sign * self.linthresh a[masked] = log a[~masked] *= self._linscale_adj @@ -1272,7 +1295,8 @@ def _inv_transform(self, a): """Inverse inplace Transformation.""" masked = np.abs(a) > (self.linthresh * self._linscale_adj) sign = np.sign(a[masked]) - exp = np.exp(sign * a[masked] / self.linthresh - self._linscale_adj) + exp = np.power(self._base, + sign * a[masked] / self.linthresh - self._linscale_adj) exp *= sign * self.linthresh a[masked] = exp a[~masked] /= self._linscale_adj diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index c0c9b6d06620..c181918519d6 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -388,7 +388,7 @@ def test_SymLogNorm(): """ Test SymLogNorm behavior """ - norm = mcolors.SymLogNorm(3, vmax=5, linscale=1.2) + norm = mcolors.SymLogNorm(3, vmax=5, linscale=1.2, base=np.e) vals = np.array([-30, -1, 2, 6], dtype=float) normed_vals = norm(vals) expected = [0., 0.53980074, 0.826991, 1.02758204] @@ -398,16 +398,30 @@ def test_SymLogNorm(): _mask_tester(norm, vals) # Ensure that specifying vmin returns the same result as above - norm = mcolors.SymLogNorm(3, vmin=-30, vmax=5, linscale=1.2) + norm = mcolors.SymLogNorm(3, vmin=-30, vmax=5, linscale=1.2, base=np.e) normed_vals = norm(vals) assert_array_almost_equal(normed_vals, expected) + # test something more easily checked. + norm = mcolors.SymLogNorm(1, vmin=-np.e**3, vmax=np.e**3, base=np.e) + nn = norm([-np.e**3, -np.e**2, -np.e**1, -1, + 0, 1, np.e**1, np.e**2, np.e**3]) + xx = np.array([0., 0.109123, 0.218246, 0.32737, 0.5, 0.67263, + 0.781754, 0.890877, 1.]) + assert_array_almost_equal(nn, xx) + norm = mcolors.SymLogNorm(1, vmin=-10**3, vmax=10**3, base=10) + nn = norm([-10**3, -10**2, -10**1, -1, + 0, 1, 10**1, 10**2, 10**3]) + xx = np.array([0., 0.121622, 0.243243, 0.364865, 0.5, 0.635135, + 0.756757, 0.878378, 1.]) + assert_array_almost_equal(nn, xx) + def test_SymLogNorm_colorbar(): """ Test un-called SymLogNorm in a colorbar. """ - norm = mcolors.SymLogNorm(0.1, vmin=-1, vmax=1, linscale=1) + norm = mcolors.SymLogNorm(0.1, vmin=-1, vmax=1, linscale=1, base=np.e) fig = plt.figure() mcolorbar.ColorbarBase(fig.add_subplot(111), norm=norm) plt.close(fig) @@ -418,7 +432,7 @@ def test_SymLogNorm_single_zero(): Test SymLogNorm to ensure it is not adding sub-ticks to zero label """ fig = plt.figure() - norm = mcolors.SymLogNorm(1e-5, vmin=-1, vmax=1) + norm = mcolors.SymLogNorm(1e-5, vmin=-1, vmax=1, base=np.e) cbar = mcolorbar.ColorbarBase(fig.add_subplot(111), norm=norm) ticks = cbar.get_ticks() assert sum(ticks == 0) == 1 @@ -895,9 +909,10 @@ def __add__(self, other): mydata = data.view(MyArray) for norm in [mcolors.Normalize(), mcolors.LogNorm(), - mcolors.SymLogNorm(3, vmax=5, linscale=1), + mcolors.SymLogNorm(3, vmax=5, linscale=1, base=np.e), mcolors.Normalize(vmin=mydata.min(), vmax=mydata.max()), - mcolors.SymLogNorm(3, vmin=mydata.min(), vmax=mydata.max()), + mcolors.SymLogNorm(3, vmin=mydata.min(), vmax=mydata.max(), + base=np.e), mcolors.PowerNorm(1)]: assert_array_equal(norm(mydata), norm(data)) fig, ax = plt.subplots() diff --git a/tutorials/colors/colormapnorms.py b/tutorials/colors/colormapnorms.py index 84244b15e6b6..833988456988 100644 --- a/tutorials/colors/colormapnorms.py +++ b/tutorials/colors/colormapnorms.py @@ -98,7 +98,7 @@ pcm = ax[0].pcolormesh(X, Y, Z, norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03, - vmin=-1.0, vmax=1.0), + vmin=-1.0, vmax=1.0, base=10), cmap='RdBu_r') fig.colorbar(pcm, ax=ax[0], extend='both')