From 0d1d95c8b4a00bfdbb0b7f02a4a0152f6fe57dcb Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 1 Dec 2019 00:04:21 +0100 Subject: [PATCH] Build lognorm/symlognorm from corresponding scales. test_contour::test_contourf_log_extension has a tick move by one pixel, but actually looks better with the patch? --- lib/matplotlib/colors.py | 263 +++++++----------- .../test_contour/contour_log_extension.png | Bin 9042 -> 8996 bytes 2 files changed, 98 insertions(+), 165 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 2cc74083103a..d6fd58c8bad9 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -68,6 +68,7 @@ import base64 from collections.abc import Sized import functools +import inspect import io import itertools from numbers import Number @@ -77,8 +78,7 @@ import matplotlib as mpl import numpy as np -import matplotlib.cbook as cbook -from matplotlib import docstring +from matplotlib import cbook, docstring, scale from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS @@ -1203,60 +1203,84 @@ class DivergingNorm(TwoSlopeNorm): ... -class LogNorm(Normalize): - """Normalize a given value to the 0-1 range on a log scale.""" +def _make_norm_from_scale(scale_cls, base_norm_cls=None, *, init=None): + """ + Decorator for building a `.Normalize` subclass from a `.Scale` subclass. - def _check_vmin_vmax(self): - if self.vmin > self.vmax: - raise ValueError("minvalue must be less than or equal to maxvalue") - elif self.vmin <= 0: - raise ValueError("minvalue must be positive") + After :: - def __call__(self, value, clip=None): - if clip is None: - clip = self.clip + @_make_norm_from_scale(scale_cls) + class base_norm_cls(Normalize): + ... - result, is_scalar = self.process_value(value) + *base_norm_cls* is filled with methods so that normalization computations + are forwarded to *scale_cls* (i.e., *scale_cls* is the scale that would be + used for the colorbar of a mappable normalized with *base_norm_cls*). - result = np.ma.masked_less_equal(result, 0, copy=False) + The constructor signature of *base_norm_cls* is derived from the + constructor signature of *scale_cls*, but can be overridden using *init* + (a callable which is *only* used for its signature). + """ - self.autoscale_None(result) - self._check_vmin_vmax() - vmin, vmax = self.vmin, self.vmax - if vmin == vmax: - result.fill(0) - else: + if base_norm_cls is None: + return functools.partial(_make_norm_from_scale, scale_cls, init=init) + + if init is None: + def init(vmin=None, vmax=None, clip=False): pass + init_signature = inspect.signature(init) + + class Norm(base_norm_cls): + + def __init__(self, *args, **kwargs): + ba = init_signature.bind(*args, **kwargs) + ba.apply_defaults() + super().__init__( + **{k: ba.arguments.pop(k) for k in ["vmin", "vmax", "clip"]}) + self._scale = scale_cls(axis=None, **ba.arguments) + self._trf = self._scale.get_transform() + self._inv_trf = self._trf.inverted() + + def __call__(self, value, clip=None): + value, is_scalar = self.process_value(value) + self.autoscale_None(value) + if self.vmin > self.vmax: + raise ValueError("vmin must be less or equal to vmax") + if self.vmin == self.vmax: + return np.full_like(value, 0) + if clip is None: + clip = self.clip if clip: - mask = np.ma.getmask(result) - result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax), - mask=mask) - # in-place equivalent of above can be much faster - resdat = result.data - mask = result.mask - if mask is np.ma.nomask: - mask = (resdat <= 0) - else: - mask |= resdat <= 0 - np.copyto(resdat, 1, where=mask) - np.log(resdat, resdat) - resdat -= np.log(vmin) - resdat /= (np.log(vmax) - np.log(vmin)) - result = np.ma.array(resdat, mask=mask, copy=False) - if is_scalar: - result = result[0] - return result - - def inverse(self, value): - if not self.scaled(): - raise ValueError("Not invertible until scaled") - self._check_vmin_vmax() - vmin, vmax = self.vmin, self.vmax - - if np.iterable(value): - val = np.ma.asarray(value) - return vmin * np.ma.power((vmax / vmin), val) - else: - return vmin * pow((vmax / vmin), value) + value = np.clip(value, self.vmin, self.vmax) + t_value = self._trf.transform(value).reshape(np.shape(value)) + t_vmin, t_vmax = self._trf.transform([self.vmin, self.vmax]) + if not np.isfinite([t_vmin, t_vmax]).all(): + raise ValueError("Invalid vmin or vmax") + t_value -= t_vmin + t_value /= (t_vmax - t_vmin) + t_value = np.ma.masked_invalid(t_value, copy=False) + return t_value[0] if is_scalar else t_value + + def inverse(self, value): + if not self.scaled(): + raise ValueError("Not invertible until scaled") + if self.vmin > self.vmax: + raise ValueError("vmin must be less or equal to vmax") + t_vmin, t_vmax = self._trf.transform([self.vmin, self.vmax]) + if not np.isfinite([t_vmin, t_vmax]).all(): + raise ValueError("Invalid vmin or vmax") + rescaled = value * (t_vmax - t_vmin) + rescaled += t_vmin + return self._inv_trf.transform(rescaled).reshape(np.shape(value)) + + Norm.__name__ = base_norm_cls.__name__ + Norm.__qualname__ = base_norm_cls.__qualname__ + Norm.__module__ = base_norm_cls.__module__ + return Norm + + +@_make_norm_from_scale(functools.partial(scale.LogScale, nonpositive="mask")) +class LogNorm(Normalize): + """Normalize a given value to the 0-1 range on a log scale.""" def autoscale(self, A): # docstring inherited. @@ -1267,6 +1291,10 @@ def autoscale_None(self, A): super().autoscale_None(np.ma.masked_less_equal(A, 0, copy=False)) +@_make_norm_from_scale( + scale.SymmetricalLogScale, + init=lambda linthresh, linscale=1., vmin=None, vmax=None, clip=False, *, + base=10: None) class SymLogNorm(Normalize): """ The symmetrical logarithmic scale is logarithmic in both the @@ -1276,124 +1304,29 @@ class SymLogNorm(Normalize): need to have a range around zero that is linear. The parameter *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, *, base=None): - """ - 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.2", removal="3.4", message="default base will change from " - "np.e to 10 %(removal)s. 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 - self._base ** -1)) - if vmin is not None and vmax is not None: - self._transform_vmin_vmax() - - def __call__(self, value, clip=None): - if clip is None: - clip = self.clip - - result, is_scalar = self.process_value(value) - self.autoscale_None(result) - vmin, vmax = self.vmin, self.vmax - - if vmin > vmax: - raise ValueError("minvalue must be less than or equal to maxvalue") - elif vmin == vmax: - result.fill(0) - else: - if clip: - mask = np.ma.getmask(result) - result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax), - mask=mask) - # in-place equivalent of above can be much faster - resdat = self._transform(result.data) - resdat -= self._lower - resdat /= (self._upper - self._lower) - - if is_scalar: - result = result[0] - return result - - def _transform(self, a): - """Inplace transformation.""" - 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) / self._log_base) - log *= sign * self.linthresh - a[masked] = log - a[~masked] *= self._linscale_adj - return a - - def _inv_transform(self, a): - """Inverse inplace Transformation.""" - masked = np.abs(a) > (self.linthresh * self._linscale_adj) - sign = np.sign(a[masked]) - 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 - return a - - def _transform_vmin_vmax(self): - """Calculate vmin and vmax in the transformed system.""" - vmin, vmax = self.vmin, self.vmax - arr = np.array([vmax, vmin]).astype(float) - self._upper, self._lower = self._transform(arr) - - def inverse(self, value): - if not self.scaled(): - raise ValueError("Not invertible until scaled") - val = np.ma.asarray(value) - val = val * (self._upper - self._lower) + self._lower - return self._inv_transform(val) + 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 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. + base : float, default: 10 + """ - def autoscale(self, A): - # docstring inherited. - super().autoscale(A) - self._transform_vmin_vmax() + @property + def linthresh(self): + return self._scale.linthresh - def autoscale_None(self, A): - # docstring inherited. - super().autoscale_None(A) - self._transform_vmin_vmax() + @linthresh.setter + def linthresh(self, value): + self._scale.linthresh = value class PowerNorm(Normalize): diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_log_extension.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_log_extension.png index 1a870771b95be50a9caeb940b89f96c4bd4be406..c1feda1e9cbae3be3b5abf79225ed0316fb73c3a 100644 GIT binary patch literal 8996 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 Nbc~K?9<{mgzW^%YUF`q>