Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plot limit with transform #731

Merged
merged 11 commits into from Aug 21, 2012
48 changes: 48 additions & 0 deletions doc/api/api_changes.rst
Expand Up @@ -72,6 +72,54 @@ Changes in 1.2.x
original keyword arguments will override any value provided by original keyword arguments will override any value provided by
*capthick*. *capthick*.


* Transform subclassing behaviour is now subtly changed. If your transform
implements a non-affine transformation, then it should override the
``transform_non_affine`` method, rather than the generic ``transform`` method.
Previously transforms would define ``transform`` and then copy the
method into ``transform_non_affine``:

class MyTransform(mtrans.Transform):
def transform(self, xy):
...
transform_non_affine = transform

This approach will no longer function correctly and should be changed to:

class MyTransform(mtrans.Transform):
def transform_non_affine(self, xy):
...

* Artists no longer have ``x_isdata`` or ``y_isdata`` attributes; instead
any artist's transform can be interrogated with
``artist_instance.get_transform().contains_branch(ax.transData)``

* Lines added to an axes now take into account their transform when updating the
data and view limits. This means transforms can now be used as a pre-transform.
For instance:

>>> import matplotlib.pyplot as plt
>>> import matplotlib.transforms as mtrans
>>> ax = plt.axes()
>>> ax.plot(range(10), transform=mtrans.Affine2D().scale(10) + ax.transData)
>>> print(ax.viewLim)
Bbox('array([[ 0., 0.],\n [ 90., 90.]])')

* One can now easily get a transform which goes from one transform's coordinate system
to another, in an optimized way, using the new subtract method on a transform. For instance,
to go from data coordinates to axes coordinates::

>>> import matplotlib.pyplot as plt
>>> ax = plt.axes()
>>> data2ax = ax.transData - ax.transAxes
>>> print(ax.transData.depth, ax.transAxes.depth)
3, 1
>>> print(data2ax.depth)
2

for versions before 1.2 this could only be achieved in a sub-optimal way, using
``ax.transData + ax.transAxes.inverted()`` (depth is a new concept, but had it existed
it would return 4 for this example).

Changes in 1.1.x Changes in 1.1.x
================ ================


Expand Down
2 changes: 0 additions & 2 deletions lib/matplotlib/artist.py
Expand Up @@ -101,8 +101,6 @@ def __init__(self):
self._remove_method = None self._remove_method = None
self._url = None self._url = None
self._gid = None self._gid = None
self.x_isdata = True # False to avoid updating Axes.dataLim with x
self.y_isdata = True # with y
self._snap = None self._snap = None


def remove(self): def remove(self):
Expand Down
69 changes: 51 additions & 18 deletions lib/matplotlib/axes.py
Expand Up @@ -1461,17 +1461,52 @@ def add_line(self, line):


self._update_line_limits(line) self._update_line_limits(line)
if not line.get_label(): if not line.get_label():
line.set_label('_line%d'%len(self.lines)) line.set_label('_line%d' % len(self.lines))
self.lines.append(line) self.lines.append(line)
line._remove_method = lambda h: self.lines.remove(h) line._remove_method = lambda h: self.lines.remove(h)
return line return line


def _update_line_limits(self, line): def _update_line_limits(self, line):
p = line.get_path() """Figures out the data limit of the given line, updating self.dataLim."""
if p.vertices.size > 0: path = line.get_path()
self.dataLim.update_from_path(p, self.ignore_existing_data_limits, if path.vertices.size == 0:
updatex=line.x_isdata, return
updatey=line.y_isdata)
line_trans = line.get_transform()

if line_trans == self.transData:
data_path = path

elif any(line_trans.contains_branch_seperately(self.transData)):
# identify the transform to go from line's coordinates
# to data coordinates
trans_to_data = line_trans - self.transData

# if transData is affine we can use the cached non-affine component
# of line's path. (since the non-affine part of line_trans is
# entirely encapsulated in trans_to_data).
if self.transData.is_affine:
line_trans_path = line._get_transformed_path()
na_path, _ = line_trans_path.get_transformed_path_and_affine()
data_path = trans_to_data.transform_path_affine(na_path)
else:
data_path = trans_to_data.transform_path(path)
else:
# for backwards compatibility we update the dataLim with the
# coordinate range of the given path, even though the coordinate
# systems are completely different. This may occur in situations
# such as when ax.transAxes is passed through for absolute
# positioning.
data_path = path

if data_path.vertices.size > 0:
updatex, updatey = line_trans.contains_branch_seperately(
self.transData
)
self.dataLim.update_from_path(data_path,
self.ignore_existing_data_limits,
updatex=updatex,
updatey=updatey)
self.ignore_existing_data_limits = False self.ignore_existing_data_limits = False


def add_patch(self, p): def add_patch(self, p):
Expand Down Expand Up @@ -1507,11 +1542,14 @@ def _update_patch_limits(self, patch):
if vertices.size > 0: if vertices.size > 0:
xys = patch.get_patch_transform().transform(vertices) xys = patch.get_patch_transform().transform(vertices)
if patch.get_data_transform() != self.transData: if patch.get_data_transform() != self.transData:
transform = (patch.get_data_transform() + patch_to_data = (patch.get_data_transform() -
self.transData.inverted()) self.transData)
xys = transform.transform(xys) xys = patch_to_data.transform(xys)
self.update_datalim(xys, updatex=patch.x_isdata,
updatey=patch.y_isdata) updatex, updatey = patch.get_transform().\
contains_branch_seperately(self.transData)
self.update_datalim(xys, updatex=updatex,
updatey=updatey)




def add_table(self, tab): def add_table(self, tab):
Expand Down Expand Up @@ -1599,13 +1637,13 @@ def _process_unit_info(self, xdata=None, ydata=None, kwargs=None):
if xdata is not None: if xdata is not None:
# we only need to update if there is nothing set yet. # we only need to update if there is nothing set yet.
if not self.xaxis.have_units(): if not self.xaxis.have_units():
self.xaxis.update_units(xdata) self.xaxis.update_units(xdata)
#print '\tset from xdata', self.xaxis.units #print '\tset from xdata', self.xaxis.units


if ydata is not None: if ydata is not None:
# we only need to update if there is nothing set yet. # we only need to update if there is nothing set yet.
if not self.yaxis.have_units(): if not self.yaxis.have_units():
self.yaxis.update_units(ydata) self.yaxis.update_units(ydata)
#print '\tset from ydata', self.yaxis.units #print '\tset from ydata', self.yaxis.units


# process kwargs 2nd since these will override default units # process kwargs 2nd since these will override default units
Expand Down Expand Up @@ -3424,7 +3462,6 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs):
trans = mtransforms.blended_transform_factory( trans = mtransforms.blended_transform_factory(
self.transAxes, self.transData) self.transAxes, self.transData)
l = mlines.Line2D([xmin,xmax], [y,y], transform=trans, **kwargs) l = mlines.Line2D([xmin,xmax], [y,y], transform=trans, **kwargs)
l.x_isdata = False
self.add_line(l) self.add_line(l)
self.autoscale_view(scalex=False, scaley=scaley) self.autoscale_view(scalex=False, scaley=scaley)
return l return l
Expand Down Expand Up @@ -3489,7 +3526,6 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs):
trans = mtransforms.blended_transform_factory( trans = mtransforms.blended_transform_factory(
self.transData, self.transAxes) self.transData, self.transAxes)
l = mlines.Line2D([x,x], [ymin,ymax] , transform=trans, **kwargs) l = mlines.Line2D([x,x], [ymin,ymax] , transform=trans, **kwargs)
l.y_isdata = False
self.add_line(l) self.add_line(l)
self.autoscale_view(scalex=scalex, scaley=False) self.autoscale_view(scalex=scalex, scaley=False)
return l return l
Expand Down Expand Up @@ -3546,7 +3582,6 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs):
verts = (xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin) verts = (xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)
p = mpatches.Polygon(verts, **kwargs) p = mpatches.Polygon(verts, **kwargs)
p.set_transform(trans) p.set_transform(trans)
p.x_isdata = False
self.add_patch(p) self.add_patch(p)
self.autoscale_view(scalex=False) self.autoscale_view(scalex=False)
return p return p
Expand Down Expand Up @@ -3603,7 +3638,6 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs):
verts = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] verts = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]
p = mpatches.Polygon(verts, **kwargs) p = mpatches.Polygon(verts, **kwargs)
p.set_transform(trans) p.set_transform(trans)
p.y_isdata = False
self.add_patch(p) self.add_patch(p)
self.autoscale_view(scaley=False) self.autoscale_view(scaley=False)
return p return p
Expand Down Expand Up @@ -3909,7 +3943,6 @@ def plot(self, *args, **kwargs):
self.add_line(line) self.add_line(line)
lines.append(line) lines.append(line)



self.autoscale_view(scalex=scalex, scaley=scaley) self.autoscale_view(scalex=scalex, scaley=scaley)
return lines return lines


Expand Down
31 changes: 22 additions & 9 deletions lib/matplotlib/lines.py
Expand Up @@ -6,6 +6,8 @@
# TODO: expose cap and join style attrs # TODO: expose cap and join style attrs
from __future__ import division, print_function from __future__ import division, print_function


import warnings

import numpy as np import numpy as np
from numpy import ma from numpy import ma
from matplotlib import verbose from matplotlib import verbose
Expand Down Expand Up @@ -249,17 +251,15 @@ def contains(self, mouseevent):
if len(self._xy)==0: return False,{} if len(self._xy)==0: return False,{}


# Convert points to pixels # Convert points to pixels
if self._transformed_path is None: path, affine = self._get_transformed_path().get_transformed_path_and_affine()
self._transform_path()
path, affine = self._transformed_path.get_transformed_path_and_affine()
path = affine.transform_path(path) path = affine.transform_path(path)
xy = path.vertices xy = path.vertices
xt = xy[:, 0] xt = xy[:, 0]
yt = xy[:, 1] yt = xy[:, 1]


# Convert pick radius from points to pixels # Convert pick radius from points to pixels
if self.figure == None: if self.figure is None:
warning.warn('no figure set when check if mouse is on line') warnings.warn('no figure set when check if mouse is on line')
pixels = self.pickradius pixels = self.pickradius
else: else:
pixels = self.figure.dpi/72. * self.pickradius pixels = self.figure.dpi/72. * self.pickradius
Expand Down Expand Up @@ -446,13 +446,26 @@ def recache(self, always=False):
self._invalidy = False self._invalidy = False


def _transform_path(self, subslice=None): def _transform_path(self, subslice=None):
"""
Puts a TransformedPath instance at self._transformed_path,
all invalidation of the transform is then handled by the
TransformedPath instance.
"""
# Masked arrays are now handled by the Path class itself # Masked arrays are now handled by the Path class itself
if subslice is not None: if subslice is not None:
_path = Path(self._xy[subslice,:]) _path = Path(self._xy[subslice,:])
else: else:
_path = self._path _path = self._path
self._transformed_path = TransformedPath(_path, self.get_transform()) self._transformed_path = TransformedPath(_path, self.get_transform())


def _get_transformed_path(self):
"""
Return the :class:`~matplotlib.transforms.TransformedPath` instance
of this line.
"""
if self._transformed_path is None:
self._transform_path()
return self._transformed_path


def set_transform(self, t): def set_transform(self, t):
""" """
Expand Down Expand Up @@ -482,8 +495,8 @@ def draw(self, renderer):
subslice = slice(max(i0-1, 0), i1+1) subslice = slice(max(i0-1, 0), i1+1)
self.ind_offset = subslice.start self.ind_offset = subslice.start
self._transform_path(subslice) self._transform_path(subslice)
if self._transformed_path is None:
self._transform_path() transformed_path = self._get_transformed_path()


if not self.get_visible(): return if not self.get_visible(): return


Expand All @@ -507,7 +520,7 @@ def draw(self, renderer):


funcname = self._lineStyles.get(self._linestyle, '_draw_nothing') funcname = self._lineStyles.get(self._linestyle, '_draw_nothing')
if funcname != '_draw_nothing': if funcname != '_draw_nothing':
tpath, affine = self._transformed_path.get_transformed_path_and_affine() tpath, affine = transformed_path.get_transformed_path_and_affine()
if len(tpath.vertices): if len(tpath.vertices):
self._lineFunc = getattr(self, funcname) self._lineFunc = getattr(self, funcname)
funcname = self.drawStyles.get(self._drawstyle, '_draw_lines') funcname = self.drawStyles.get(self._drawstyle, '_draw_lines')
Expand All @@ -528,7 +541,7 @@ def draw(self, renderer):
gc.set_linewidth(self._markeredgewidth) gc.set_linewidth(self._markeredgewidth)
gc.set_alpha(self._alpha) gc.set_alpha(self._alpha)
marker = self._marker marker = self._marker
tpath, affine = self._transformed_path.get_transformed_points_and_affine() tpath, affine = transformed_path.get_transformed_points_and_affine()
if len(tpath.vertices): if len(tpath.vertices):
# subsample the markers if markevery is not None # subsample the markers if markevery is not None
markevery = self.get_markevery() markevery = self.get_markevery()
Expand Down
12 changes: 12 additions & 0 deletions lib/matplotlib/patches.py
Expand Up @@ -167,9 +167,21 @@ def get_transform(self):
return self.get_patch_transform() + artist.Artist.get_transform(self) return self.get_patch_transform() + artist.Artist.get_transform(self)


def get_data_transform(self): def get_data_transform(self):
"""
Return the :class:`~matplotlib.transforms.Transform` instance which
maps data coordinates to physical coordinates.
"""
return artist.Artist.get_transform(self) return artist.Artist.get_transform(self)


def get_patch_transform(self): def get_patch_transform(self):
"""
Return the :class:`~matplotlib.transforms.Transform` instance which
takes patch coordinates to data coordinates.

For example, one may define a patch of a circle which represents a
radius of 5 by providing coordinates for a unit circle, and a
transform which scales the coordinates (the patch coordinate) by 5.
"""
return transforms.IdentityTransform() return transforms.IdentityTransform()


def get_antialiased(self): def get_antialiased(self):
Expand Down