diff --git a/doc/users/next_whats_new/20171119-transforms-repr.rst b/doc/users/next_whats_new/20171119-transforms-repr.rst new file mode 100644 index 000000000000..2389141b6492 --- /dev/null +++ b/doc/users/next_whats_new/20171119-transforms-repr.rst @@ -0,0 +1,32 @@ +Improved `repr` for `Transform`\s +--------------------------------- + +`Transform`\s now indent their `repr`\s in a more legible manner: + +.. code-block:: ipython + + In [1]: l, = plt.plot([]); l.get_transform() + Out[1]: + CompositeGenericTransform( + TransformWrapper( + BlendedAffine2D( + IdentityTransform(), + IdentityTransform())), + CompositeGenericTransform( + BboxTransformFrom( + TransformedBbox( + Bbox(x0=-0.05500000000000001, y0=-0.05500000000000001, x1=0.05500000000000001, y1=0.05500000000000001), + TransformWrapper( + BlendedAffine2D( + IdentityTransform(), + IdentityTransform())))), + BboxTransformTo( + TransformedBbox( + Bbox(x0=0.125, y0=0.10999999999999999, x1=0.9, y1=0.88), + BboxTransformTo( + TransformedBbox( + Bbox(x0=0.0, y0=0.0, x1=6.4, y1=4.8), + Affine2D( + [[ 100. 0. 0.] + [ 0. 100. 0.] + [ 0. 0. 1.]]))))))) diff --git a/lib/matplotlib/projections/geo.py b/lib/matplotlib/projections/geo.py index 926a22fa5de5..0696ce5882e1 100644 --- a/lib/matplotlib/projections/geo.py +++ b/lib/matplotlib/projections/geo.py @@ -17,13 +17,11 @@ import matplotlib.spines as mspines import matplotlib.axis as maxis from matplotlib.ticker import Formatter, Locator, NullLocator, FixedLocator, NullFormatter -from matplotlib.transforms import Affine2D, Affine2DBase, Bbox, \ - BboxTransformTo, IdentityTransform, Transform, TransformWrapper +from matplotlib.transforms import Affine2D, BboxTransformTo, Transform + class GeoAxes(Axes): - """ - An abstract base class for geographic projections - """ + """An abstract base class for geographic projections.""" class ThetaFormatter(Formatter): """ Used to format the theta tick labels. Converts the native @@ -248,25 +246,37 @@ def drag_pan(self, button, key, x, y): pass -class AitoffAxes(GeoAxes): - name = 'aitoff' +class _GeoTransform(Transform): + # Factoring out some common functionality. + input_dims = 2 + output_dims = 2 + is_separable = False - class AitoffTransform(Transform): + def __init__(self, resolution): """ - The base Aitoff transform. + Create a new geographical transform. + + Resolution is the number of steps to interpolate between each input + line segment to approximate its path in curved space. """ - input_dims = 2 - output_dims = 2 - is_separable = False + Transform.__init__(self) + self._resolution = resolution + + def __str__(self): + return "{}({})".format(type(self).__name__, self._resolution) + + def transform_path_non_affine(self, path): + vertices = path.vertices + ipath = path.interpolated(self._resolution) + return Path(self.transform(ipath.vertices), ipath.codes) + transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ - def __init__(self, resolution): - """ - Create a new Aitoff transform. Resolution is the number of steps - to interpolate between each input line segment to approximate its - path in curved Aitoff space. - """ - Transform.__init__(self) - self._resolution = resolution + +class AitoffAxes(GeoAxes): + name = 'aitoff' + + class AitoffTransform(_GeoTransform): + """The base Aitoff transform.""" def transform_non_affine(self, ll): longitude = ll[:, 0:1] @@ -289,24 +299,11 @@ def transform_non_affine(self, ll): return np.concatenate((x.filled(0), y.filled(0)), 1) transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ - def transform_path_non_affine(self, path): - vertices = path.vertices - ipath = path.interpolated(self._resolution) - return Path(self.transform(ipath.vertices), ipath.codes) - transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ - def inverted(self): return AitoffAxes.InvertedAitoffTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__ - class InvertedAitoffTransform(Transform): - input_dims = 2 - output_dims = 2 - is_separable = False - - def __init__(self, resolution): - Transform.__init__(self) - self._resolution = resolution + class InvertedAitoffTransform(_GeoTransform): def transform_non_affine(self, xy): # MGDTODO: Math is hard ;( @@ -330,22 +327,8 @@ def _get_core_transform(self, resolution): class HammerAxes(GeoAxes): name = 'hammer' - class HammerTransform(Transform): - """ - The base Hammer transform. - """ - input_dims = 2 - output_dims = 2 - is_separable = False - - def __init__(self, resolution): - """ - Create a new Hammer transform. Resolution is the number of steps - to interpolate between each input line segment to approximate its - path in curved Hammer space. - """ - Transform.__init__(self) - self._resolution = resolution + class HammerTransform(_GeoTransform): + """The base Hammer transform.""" def transform_non_affine(self, ll): longitude = ll[:, 0:1] @@ -362,24 +345,11 @@ def transform_non_affine(self, ll): return np.concatenate((x, y), 1) transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ - def transform_path_non_affine(self, path): - vertices = path.vertices - ipath = path.interpolated(self._resolution) - return Path(self.transform(ipath.vertices), ipath.codes) - transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ - def inverted(self): return HammerAxes.InvertedHammerTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__ - class InvertedHammerTransform(Transform): - input_dims = 2 - output_dims = 2 - is_separable = False - - def __init__(self, resolution): - Transform.__init__(self) - self._resolution = resolution + class InvertedHammerTransform(_GeoTransform): def transform_non_affine(self, xy): x, y = xy.T @@ -406,22 +376,8 @@ def _get_core_transform(self, resolution): class MollweideAxes(GeoAxes): name = 'mollweide' - class MollweideTransform(Transform): - """ - The base Mollweide transform. - """ - input_dims = 2 - output_dims = 2 - is_separable = False - - def __init__(self, resolution): - """ - Create a new Mollweide transform. Resolution is the number of steps - to interpolate between each input line segment to approximate its - path in curved Mollweide space. - """ - Transform.__init__(self) - self._resolution = resolution + class MollweideTransform(_GeoTransform): + """The base Mollweide transform.""" def transform_non_affine(self, ll): def d(theta): @@ -457,24 +413,11 @@ def d(theta): return xy transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ - def transform_path_non_affine(self, path): - vertices = path.vertices - ipath = path.interpolated(self._resolution) - return Path(self.transform(ipath.vertices), ipath.codes) - transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ - def inverted(self): return MollweideAxes.InvertedMollweideTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__ - class InvertedMollweideTransform(Transform): - input_dims = 2 - output_dims = 2 - is_separable = False - - def __init__(self, resolution): - Transform.__init__(self) - self._resolution = resolution + class InvertedMollweideTransform(_GeoTransform): def transform_non_affine(self, xy): x = xy[:, 0:1] @@ -506,13 +449,8 @@ def _get_core_transform(self, resolution): class LambertAxes(GeoAxes): name = 'lambert' - class LambertTransform(Transform): - """ - The base Lambert transform. - """ - input_dims = 2 - output_dims = 2 - is_separable = False + class LambertTransform(_GeoTransform): + """The base Lambert transform.""" def __init__(self, center_longitude, center_latitude, resolution): """ @@ -520,8 +458,7 @@ def __init__(self, center_longitude, center_latitude, resolution): to interpolate between each input line segment to approximate its path in curved Lambert space. """ - Transform.__init__(self) - self._resolution = resolution + _GeoTransform.__init__(self, resolution) self._center_longitude = center_longitude self._center_latitude = center_latitude @@ -548,12 +485,6 @@ def transform_non_affine(self, ll): return np.concatenate((x, y), 1) transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ - def transform_path_non_affine(self, path): - vertices = path.vertices - ipath = path.interpolated(self._resolution) - return Path(self.transform(ipath.vertices), ipath.codes) - transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ - def inverted(self): return LambertAxes.InvertedLambertTransform( self._center_longitude, @@ -561,14 +492,10 @@ def inverted(self): self._resolution) inverted.__doc__ = Transform.inverted.__doc__ - class InvertedLambertTransform(Transform): - input_dims = 2 - output_dims = 2 - is_separable = False + class InvertedLambertTransform(_GeoTransform): def __init__(self, center_longitude, center_latitude, resolution): - Transform.__init__(self) - self._resolution = resolution + _GeoTransform.__init__(self, resolution) self._center_longitude = center_longitude self._center_latitude = center_latitude diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 6c519a01a1a6..243e1be02371 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -39,6 +39,16 @@ def __init__(self, axis=None, use_rmin=True, self._use_rmin = use_rmin self._apply_theta_transforms = _apply_theta_transforms + def __str__(self): + return ("{}(\n" + "{},\n" + " use_rmin={},\n" + " _apply_theta_transforms={})" + .format(type(self).__name__, + mtransforms._indent_str(self._axis), + self._use_rmin, + self._apply_theta_transforms)) + def transform_non_affine(self, tr): xy = np.empty(tr.shape, float) @@ -95,6 +105,14 @@ def __init__(self, scale_transform, limits): self.set_children(scale_transform, limits) self._mtx = None + def __str__(self): + return ("{}(\n" + "{},\n" + "{})" + .format(type(self).__name__, + mtransforms._indent_str(self._scale_transform), + mtransforms._indent_str(self._limits))) + def get_matrix(self): if self._invalid: limits_scaled = self._limits.transformed(self._scale_transform) @@ -125,6 +143,16 @@ def __init__(self, axis=None, use_rmin=True, self._use_rmin = use_rmin self._apply_theta_transforms = _apply_theta_transforms + def __str__(self): + return ("{}(\n" + "{},\n" + " use_rmin={},\n" + " _apply_theta_transforms={})" + .format(type(self).__name__, + mtransforms._indent_str(self._axis), + self._use_rmin, + self._apply_theta_transforms)) + def transform_non_affine(self, xy): x = xy[:, 0:1] y = xy[:, 1:] @@ -459,6 +487,16 @@ def __init__(self, axes, pad, mode): self.mode = mode self.pad = pad + def __str__(self): + return ("{}(\n" + "{},\n" + "{},\n" + "{})" + .format(type(self).__name__, + mtransforms._indent_str(self.axes), + mtransforms._indent_str(self.pad), + mtransforms._indent_str(repr(self.mode)))) + def get_matrix(self): if self._invalid: if self.mode == 'rlabel': @@ -697,9 +735,15 @@ def __init__(self, center, viewLim, originLim, **kwargs): self._originLim = originLim self.set_children(viewLim, originLim) - def __repr__(self): - return "_WedgeBbox(%r, %r, %r)" % (self._center, self._viewLim, - self._originLim) + def __str__(self): + return ("{}(\n" + "{},\n" + "{},\n" + "{})" + .format(type(self).__name__, + mtransforms._indent_str(self._center), + mtransforms._indent_str(self._viewLim), + mtransforms._indent_str(self._originLim))) def get_points(self): if self._invalid: diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 2269c9e40276..3676b278e512 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -43,6 +43,7 @@ update_path_extents) from numpy.linalg import inv +import re import weakref import warnings @@ -52,6 +53,10 @@ DEBUG = False +def _indent_str(obj): # textwrap.indent(str(obj), 4) on Py3. + return re.sub("(^|\n)", r"\1 ", str(obj)) + + class TransformNode(object): """ :class:`TransformNode` is the base class for anything that @@ -117,7 +122,7 @@ def __setstate__(self, data_dict): def __copy__(self, *args): raise NotImplementedError( - "TransformNode instances can not be copied. " + + "TransformNode instances can not be copied. " "Consider using frozen() instead.") __deepcopy__ = __copy__ @@ -1053,8 +1058,15 @@ def __init__(self, bbox, transform, **kwargs): self.set_children(bbox, transform) self._points = None - def __repr__(self): - return "TransformedBbox(%r, %r)" % (self._bbox, self._transform) + def __str__(self): + return ("{}(\n" + "{},\n" + "{})" + .format(type(self).__name__, + _indent_str(self._bbox), + _indent_str(self._transform))) + + __repr__ = __str__ def get_points(self): if self._invalid: @@ -1134,8 +1146,15 @@ def __init__(self, bbox, x0=None, y0=None, x1=None, y1=None, **kwargs): self._locked_points = np.ma.array(fp, np.float_, mask=mask).reshape((2, 2)) - def __repr__(self): - return "LockableBbox(%r, %r)" % (self._bbox, self._locked_points) + def __str__(self): + return ("{}(\n" + "{},\n" + "{})" + .format(type(self).__name__, + _indent_str(self._bbox), + _indent_str(self._locked_points))) + + __repr__ = __str__ def get_points(self): if self._invalid: @@ -1622,6 +1641,9 @@ def inverted(self): """ raise NotImplementedError() + def __repr__(self): + return str(self) + class TransformWrapper(Transform): """ @@ -1661,11 +1683,6 @@ def _init(self, child): def __eq__(self, other): return self._child.__eq__(other) - if DEBUG: - - def __str__(self): - return str(self._child) - # NOTE: Transform.__[gs]etstate__ should be sufficient when using only # Python 3.4+. def __getstate__(self): @@ -1690,8 +1707,11 @@ def __setstate__(self, state): self._parents = dict((k, weakref.ref(v)) for (k, v) in six.iteritems(state['parents']) if v is not None) - def __repr__(self): - return "TransformWrapper(%r)" % self._child + def __str__(self): + return ("{}(\n" + "{})" + .format(type(self).__name__, + _indent_str(self._child))) def frozen(self): return self._child.frozen() @@ -1918,8 +1938,11 @@ def __init__(self, matrix=None, **kwargs): self._mtx = matrix self._invalid = 0 - def __repr__(self): - return "Affine2D(%s)" % repr(self._mtx) + def __str__(self): + return ("{}(\n" + "{})" + .format(type(self).__name__, + _indent_str(self._mtx))) @staticmethod def from_values(a, b, c, d, e, f): @@ -2123,8 +2146,9 @@ def frozen(self): return self frozen.__doc__ = Affine2DBase.frozen.__doc__ - def __repr__(self): - return "IdentityTransform()" + def __str__(self): + return ("{}()" + .format(type(self).__name__)) def get_matrix(self): return self._mtx @@ -2223,8 +2247,13 @@ def frozen(self): return blended_transform_factory(self._x.frozen(), self._y.frozen()) frozen.__doc__ = Transform.frozen.__doc__ - def __repr__(self): - return "BlendedGenericTransform(%s,%s)" % (self._x, self._y) + def __str__(self): + return ("{}(\n" + "{},\n" + "{})" + .format(type(self).__name__, + _indent_str(self._x), + _indent_str(self._y))) def transform_non_affine(self, points): if self._x.is_affine and self._y.is_affine: @@ -2328,8 +2357,13 @@ def contains_branch_seperately(self, transform): # Note, this is an exact copy of BlendedTransform.contains_branch_seperately return self._x.contains_branch(transform), self._y.contains_branch(transform) - def __repr__(self): - return "BlendedAffine2D(%s,%s)" % (self._x, self._y) + def __str__(self): + return ("{}(\n" + "{},\n" + "{})" + .format(type(self).__name__, + _indent_str(self._x), + _indent_str(self._y))) def get_matrix(self): if self._invalid: @@ -2443,12 +2477,13 @@ def _get_is_separable(self): return self._a.is_separable and self._b.is_separable is_separable = property(_get_is_separable) - if DEBUG: - def __str__(self): - return '(%s, %s)' % (self._a, self._b) - - def __repr__(self): - return "CompositeGenericTransform(%r, %r)" % (self._a, self._b) + def __str__(self): + return ("{}(\n" + "{},\n" + "{})" + .format(type(self).__name__, + _indent_str(self._a), + _indent_str(self._b))) def transform_affine(self, points): return self.get_affine().transform(points) @@ -2525,10 +2560,6 @@ def __init__(self, a, b, **kwargs): self.set_children(a, b) self._mtx = None - if DEBUG: - def __str__(self): - return '(%s, %s)' % (self._a, self._b) - @property def depth(self): return self._a.depth + self._b.depth @@ -2539,8 +2570,13 @@ def _iter_break_from_left_to_right(self): for lh_compliment, rh_compliment in self._b._iter_break_from_left_to_right(): yield self._a + lh_compliment, rh_compliment - def __repr__(self): - return "CompositeAffine2D(%r, %r)" % (self._a, self._b) + def __str__(self): + return ("{}(\n" + "{},\n" + "{})" + .format(type(self).__name__, + _indent_str(self._a), + _indent_str(self._b))) def get_matrix(self): if self._invalid: @@ -2603,8 +2639,13 @@ def __init__(self, boxin, boxout, **kwargs): self._mtx = None self._inverted = None - def __repr__(self): - return "BboxTransform(%r, %r)" % (self._boxin, self._boxout) + def __str__(self): + return ("{}(\n" + "{},\n" + "{})" + .format(type(self).__name__, + _indent_str(self._boxin), + _indent_str(self._boxout))) def get_matrix(self): if self._invalid: @@ -2646,8 +2687,11 @@ def __init__(self, boxout, **kwargs): self._mtx = None self._inverted = None - def __repr__(self): - return "BboxTransformTo(%r)" % (self._boxout) + def __str__(self): + return ("{}(\n" + "{})" + .format(type(self).__name__, + _indent_str(self._boxout))) def get_matrix(self): if self._invalid: @@ -2670,9 +2714,6 @@ class BboxTransformToMaxOnly(BboxTransformTo): transforms points from the unit bounding box to a given :class:`Bbox` with a fixed upper left of (0, 0). """ - def __repr__(self): - return "BboxTransformToMaxOnly(%r)" % (self._boxout) - def get_matrix(self): if self._invalid: xmax, ymax = self._boxout.max @@ -2705,8 +2746,11 @@ def __init__(self, boxin, **kwargs): self._mtx = None self._inverted = None - def __repr__(self): - return "BboxTransformFrom(%r)" % (self._boxin) + def __str__(self): + return ("{}(\n" + "{})" + .format(type(self).__name__, + _indent_str(self._boxin))) def get_matrix(self): if self._invalid: @@ -2738,8 +2782,11 @@ def __init__(self, xt, yt, scale_trans, **kwargs): self._mtx = None self._inverted = None - def __repr__(self): - return "ScaledTranslation(%r)" % (self._t,) + def __str__(self): + return ("{}(\n" + "{})" + .format(type(self).__name__, + _indent_str(self._t))) def get_matrix(self): if self._invalid: