diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index ce52abc67663..82228189625c 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1075,6 +1075,7 @@ def tk_window_focus(): 'matplotlib.tests.test_patches', 'matplotlib.tests.test_pickle', 'matplotlib.tests.test_rcparams', + 'matplotlib.tests.test_scale', 'matplotlib.tests.test_simplification', 'matplotlib.tests.test_spines', 'matplotlib.tests.test_text', diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index ca50e177c6db..0f824abeb572 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -1,16 +1,17 @@ from __future__ import print_function -import textwrap + import numpy as np from numpy import ma -MaskedArray = ma.MaskedArray -from cbook import dedent -from ticker import NullFormatter, ScalarFormatter, LogFormatterMathtext, Formatter -from ticker import NullLocator, LogLocator, AutoLocator, SymmetricalLogLocator, FixedLocator -from ticker import is_decade -from transforms import Transform, IdentityTransform +from matplotlib.cbook import dedent +from matplotlib.ticker import (NullFormatter, ScalarFormatter, + LogFormatterMathtext) +from matplotlib.ticker import (NullLocator, LogLocator, AutoLocator, + SymmetricalLogLocator) +from matplotlib.transforms import Transform, IdentityTransform from matplotlib import docstring + class ScaleBase(object): """ The base class for all scales. @@ -31,7 +32,7 @@ def get_transform(self): Return the :class:`~matplotlib.transforms.Transform` object associated with this scale. """ - raise NotImplementedError + raise NotImplementedError() def set_default_locators_and_formatters(self, axis): """ @@ -39,7 +40,7 @@ def set_default_locators_and_formatters(self, axis): :class:`~matplotlib.ticker.Formatter` objects on the given axis to match this scale. """ - raise NotImplementedError + raise NotImplementedError() def limit_range_for_scale(self, vmin, vmax, minpos): """ @@ -51,6 +52,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos): """ return vmin, vmax + class LinearScale(ScaleBase): """ The default linear scale. @@ -90,10 +92,12 @@ def _mask_non_positives(a): return ma.MaskedArray(a, mask=mask) return a + def _clip_non_positives(a): a[a <= 0.0] = 1e-300 return a + class LogScale(ScaleBase): """ A standard logarithmic scale. Care is taken so non-positive @@ -115,7 +119,8 @@ class LogTransformBase(Transform): input_dims = 1 output_dims = 1 is_separable = True - + has_inverse = True + def __init__(self, nonpos): Transform.__init__(self) if nonpos == 'mask': @@ -123,13 +128,12 @@ def __init__(self, nonpos): else: self._handle_nonpos = _clip_non_positives - class Log10Transform(LogTransformBase): base = 10.0 - def transform(self, a): + def transform_non_affine(self, a): a = self._handle_nonpos(a * 10.0) - if isinstance(a, MaskedArray): + if isinstance(a, ma.MaskedArray): return ma.log10(a) return np.log10(a) @@ -140,9 +144,10 @@ class InvertedLog10Transform(Transform): input_dims = 1 output_dims = 1 is_separable = True + has_inverse = True base = 10.0 - def transform(self, a): + def transform_non_affine(self, a): return ma.power(10.0, a) / 10.0 def inverted(self): @@ -151,9 +156,9 @@ def inverted(self): class Log2Transform(LogTransformBase): base = 2.0 - def transform(self, a): + def transform_non_affine(self, a): a = self._handle_nonpos(a * 2.0) - if isinstance(a, MaskedArray): + if isinstance(a, ma.MaskedArray): return ma.log(a) / np.log(2) return np.log2(a) @@ -164,9 +169,10 @@ class InvertedLog2Transform(Transform): input_dims = 1 output_dims = 1 is_separable = True + has_inverse = True base = 2.0 - def transform(self, a): + def transform_non_affine(self, a): return ma.power(2.0, a) / 2.0 def inverted(self): @@ -175,9 +181,9 @@ def inverted(self): class NaturalLogTransform(LogTransformBase): base = np.e - def transform(self, a): + def transform_non_affine(self, a): a = self._handle_nonpos(a * np.e) - if isinstance(a, MaskedArray): + if isinstance(a, ma.MaskedArray): return ma.log(a) return np.log(a) @@ -188,9 +194,10 @@ class InvertedNaturalLogTransform(Transform): input_dims = 1 output_dims = 1 is_separable = True + has_inverse = True base = np.e - def transform(self, a): + def transform_non_affine(self, a): return ma.power(np.e, a) / np.e def inverted(self): @@ -200,7 +207,8 @@ class LogTransform(Transform): input_dims = 1 output_dims = 1 is_separable = True - + has_inverse = True + def __init__(self, base, nonpos): Transform.__init__(self) self.base = base @@ -209,9 +217,9 @@ def __init__(self, base, nonpos): else: self._handle_nonpos = _clip_non_positives - def transform(self, a): + def transform_non_affine(self, a): a = self._handle_nonpos(a * self.base) - if isinstance(a, MaskedArray): + if isinstance(a, ma.MaskedArray): return ma.log(a) / np.log(self.base) return np.log(a) / np.log(self.base) @@ -222,18 +230,18 @@ class InvertedLogTransform(Transform): input_dims = 1 output_dims = 1 is_separable = True - + has_inverse = True + def __init__(self, base): Transform.__init__(self) self.base = base - def transform(self, a): + def transform_non_affine(self, a): return ma.power(self.base, a) / self.base def inverted(self): return LogScale.LogTransform(self.base) - - + def __init__(self, axis, **kwargs): """ *basex*/*basey*: @@ -316,7 +324,8 @@ class SymmetricalLogTransform(Transform): input_dims = 1 output_dims = 1 is_separable = True - + has_inverse = True + def __init__(self, base, linthresh, linscale): Transform.__init__(self) self.base = base @@ -325,7 +334,7 @@ def __init__(self, base, linthresh, linscale): self._linscale_adj = (linscale / (1.0 - self.base ** -1)) self._log_base = np.log(base) - def transform(self, a): + def transform_non_affine(self, a): sign = np.sign(a) masked = ma.masked_inside(a, -self.linthresh, self.linthresh, copy=False) log = sign * self.linthresh * ( @@ -344,7 +353,8 @@ class InvertedSymmetricalLogTransform(Transform): input_dims = 1 output_dims = 1 is_separable = True - + has_inverse = True + def __init__(self, base, linthresh, linscale): Transform.__init__(self) symlog = SymmetricalLogScale.SymmetricalLogTransform(base, linthresh, linscale) @@ -354,7 +364,7 @@ def __init__(self, base, linthresh, linscale): self.linscale = linscale self._linscale_adj = (linscale / (1.0 - self.base ** -1)) - def transform(self, a): + def transform_non_affine(self, a): sign = np.sign(a) masked = ma.masked_inside(a, -self.invlinthresh, self.invlinthresh, copy=False) exp = sign * self.linthresh * ( @@ -434,17 +444,19 @@ def get_transform(self): return self._transform - _scale_mapping = { 'linear' : LinearScale, 'log' : LogScale, 'symlog' : SymmetricalLogScale } + + def get_scale_names(): names = _scale_mapping.keys() names.sort() return names + def scale_factory(scale, axis, **kwargs): """ Return a scale class by name. @@ -462,6 +474,7 @@ def scale_factory(scale, axis, **kwargs): scale_factory.__doc__ = dedent(scale_factory.__doc__) % \ {'names': " | ".join(get_scale_names())} + def register_scale(scale_class): """ Register a new kind of scale. @@ -470,6 +483,7 @@ def register_scale(scale_class): """ _scale_mapping[scale_class.name] = scale_class + def get_scale_docs(): """ Helper function for generating docstrings related to scales. @@ -486,6 +500,7 @@ def get_scale_docs(): docs.append("") return "\n".join(docs) + docstring.interpd.update( scale = ' | '.join([repr(x) for x in get_scale_names()]), scale_docs = get_scale_docs().strip(), diff --git a/lib/matplotlib/tests/baseline_images/test_scale/log_scales.pdf b/lib/matplotlib/tests/baseline_images/test_scale/log_scales.pdf new file mode 100644 index 000000000000..2ad9a721436c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_scale/log_scales.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_scale/log_scales.png b/lib/matplotlib/tests/baseline_images/test_scale/log_scales.png new file mode 100644 index 000000000000..d51f709b2b9f Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_scale/log_scales.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_scale/log_scales.svg b/lib/matplotlib/tests/baseline_images/test_scale/log_scales.svg new file mode 100644 index 000000000000..9c6f9cfecafb --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_scale/log_scales.svg @@ -0,0 +1,712 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py new file mode 100644 index 000000000000..fd705834fa26 --- /dev/null +++ b/lib/matplotlib/tests/test_scale.py @@ -0,0 +1,12 @@ +from __future__ import print_function + +from matplotlib.testing.decorators import image_comparison +import matplotlib.pyplot as plt + + +@image_comparison(baseline_images=['log_scales'], remove_text=True) +def test_log_scales(): + ax = plt.subplot(122, yscale='log', xscale='symlog') + + ax.axvline(24.1) + ax.axhline(24.1) \ No newline at end of file diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index eb8435a954bf..515a51d3c864 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -1943,18 +1943,18 @@ def transform_non_affine(self, points): y = self._y if x == y and x.input_dims == 2: - return x.transform(points) + return x.transform_non_affine(points) if x.input_dims == 2: - x_points = x.transform(points)[:, 0:1] + x_points = x.transform_non_affine(points)[:, 0:1] else: - x_points = x.transform(points[:, 0]) + x_points = x.transform_non_affine(points[:, 0]) x_points = x_points.reshape((len(x_points), 1)) if y.input_dims == 2: - y_points = y.transform(points)[:, 1:] + y_points = y.transform_non_affine(points)[:, 1:] else: - y_points = y.transform(points[:, 1]) + y_points = y.transform_non_affine(points[:, 1]) y_points = y_points.reshape((len(y_points), 1)) if isinstance(x_points, MaskedArray) or isinstance(y_points, MaskedArray): @@ -1969,19 +1969,16 @@ def inverted(self): def get_affine(self): if self._invalid or self._affine is None: - if self._x.is_affine and self._y.is_affine: - if self._x == self._y: - self._affine = self._x.get_affine() - else: - x_mtx = self._x.get_affine().get_matrix() - y_mtx = self._y.get_affine().get_matrix() - # This works because we already know the transforms are - # separable, though normally one would want to set b and - # c to zero. - mtx = np.vstack((x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0])) - self._affine = Affine2D(mtx) + if self._x == self._y: + self._affine = self._x.get_affine() else: - self._affine = IdentityTransform() + x_mtx = self._x.get_affine().get_matrix() + y_mtx = self._y.get_affine().get_matrix() + # This works because we already know the transforms are + # separable, though normally one would want to set b and + # c to zero. + mtx = np.vstack((x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0])) + self._affine = Affine2D(mtx) self._invalid = 0 return self._affine get_affine.__doc__ = Transform.get_affine.__doc__