Skip to content

Commit

Permalink
Merge pull request #731 from pelson/plot_limit_with_transform
Browse files Browse the repository at this point in the history
Plot limit with transform
  • Loading branch information
mdboom committed Aug 21, 2012
2 parents 22e6070 + 4b0fbb5 commit 98f6eb2
Show file tree
Hide file tree
Showing 9 changed files with 745 additions and 239 deletions.
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
*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
================

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._url = 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

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)
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)
line._remove_method = lambda h: self.lines.remove(h)
return line

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

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

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

updatex, updatey = patch.get_transform().\
contains_branch_seperately(self.transData)
self.update_datalim(xys, updatex=updatex,
updatey=updatey)


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:
# we only need to update if there is nothing set yet.
if not self.xaxis.have_units():
self.xaxis.update_units(xdata)
self.xaxis.update_units(xdata)
#print '\tset from xdata', self.xaxis.units

if ydata is not None:
# we only need to update if there is nothing set yet.
if not self.yaxis.have_units():
self.yaxis.update_units(ydata)
self.yaxis.update_units(ydata)
#print '\tset from ydata', self.yaxis.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(
self.transAxes, self.transData)
l = mlines.Line2D([xmin,xmax], [y,y], transform=trans, **kwargs)
l.x_isdata = False
self.add_line(l)
self.autoscale_view(scalex=False, scaley=scaley)
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(
self.transData, self.transAxes)
l = mlines.Line2D([x,x], [ymin,ymax] , transform=trans, **kwargs)
l.y_isdata = False
self.add_line(l)
self.autoscale_view(scalex=scalex, scaley=False)
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)
p = mpatches.Polygon(verts, **kwargs)
p.set_transform(trans)
p.x_isdata = False
self.add_patch(p)
self.autoscale_view(scalex=False)
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)]
p = mpatches.Polygon(verts, **kwargs)
p.set_transform(trans)
p.y_isdata = False
self.add_patch(p)
self.autoscale_view(scaley=False)
return p
Expand Down Expand Up @@ -3909,7 +3943,6 @@ def plot(self, *args, **kwargs):
self.add_line(line)
lines.append(line)


self.autoscale_view(scalex=scalex, scaley=scaley)
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
from __future__ import division, print_function

import warnings

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

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

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

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
if subslice is not None:
_path = Path(self._xy[subslice,:])
else:
_path = self._path
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):
"""
Expand Down Expand Up @@ -482,8 +495,8 @@ def draw(self, renderer):
subslice = slice(max(i0-1, 0), i1+1)
self.ind_offset = subslice.start
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

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

funcname = self._lineStyles.get(self._linestyle, '_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):
self._lineFunc = getattr(self, funcname)
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_alpha(self._alpha)
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):
# subsample the markers if markevery is not None
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)

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)

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()

def get_antialiased(self):
Expand Down

0 comments on commit 98f6eb2

Please sign in to comment.