8000 Added the "cleared" method to Path, and updated the path module's documentation. by pelson · Pull Request #2011 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Added the "cleared" method to Path, and updated the path module's documentation. #2011

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

Merged
merged 2 commits into from
May 21, 2013
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Added the cleared method to Path, and updated the path module's docum…
…entation.
  • Loading branch information
pelson committed May 21, 2013
commit 5218f474d6368164ae867ee30831707ff0c744dd
9 changes: 9 additions & 0 deletions doc/api/api_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ Changes in 1.3.x
existing text objects. This brings their behavior in line with most
other rcParams.

* To support XKCD style plots, the :func:`matplotlib.path.cleanup_path`
method's signature was updated to require a sketch argument. Users of
:func:`matplotlib.path.cleanup_path` are encouraged to use the new
:meth:`~matplotlib.path.Path.cleaned` Path method.

* The list at ``Path.NUM_VERTICES`` was replaced by a dictionary mapping
Path codes to the number of expected vertices at
:attr:`~matplotlib.path.Path.NUM_VERTICES_FOR_CODE`.

* Fixed a bug in setting the position for the right/top spine with data
position type. Previously, it would draw the right or top spine at
+1 data offset.
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/cm.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,9 @@ def __init__(self, norm=None, cmap=None):
norm = colors.Normalize()

self._A = None;
#; The Normalization instance of this ScalarMappable.
#: The Normalization instance of this ScalarMappable.
self.norm = norm
#; The Colormap instance of this ScalarMappable.
#: The Colormap instance of this ScalarMappable.
self.cmap = get_cmap(cmap)
self.colorbar = None
self.update_dict = {'array': False}
Expand Down
245 changes: 156 additions & 89 deletions 10000 lib/matplotlib/path.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
"""
Contains a class for managing paths (polylines).
A module for dealing with the polylines used throughout matplotlib.

The primary class for polyline handling in matplotlib is :class:`Path`.
Almost all vector drawing makes use of Paths somewhere in the drawing
pipeline.

Whilst a :class:`Path` instance itself cannot be drawn, there exists
:class:`~matplotlib.artist.Artist` subclasses which can be used for
convenient Path visualisation - the two most frequently used of these are
:class:`~matplotlib.patches.PathPatch` and
:class:`~matplotlib.collections.PathCollection`.
"""

from __future__ import print_function
Expand Down Expand Up @@ -53,9 +63,9 @@ class Path(object):

Users of Path objects should not access the vertices and codes
arrays directly. Instead, they should use :meth:`iter_segments`
to get the vertex/code pairs. This is important, since many
:class:`Path` objects, as an optimization, do not store a *codes*
at all, but have a default one provided for them by
or :meth:`cleaned` to get the vertex/code pairs. This is important,
since many :class:`Path` objects, as an optimization, do not store a
*codes* at all, but have a default one provided for them by
:meth:`iter_segments`.

.. note::
Expand All @@ -73,12 +83,16 @@ class Path(object):
LINETO = 2 # 1 vertex
CURVE3 = 3 # 2 vertices
CURVE4 = 4 # 3 vertices
CLOSEPOLY = 0x4f # 1 vertex
CLOSEPOLY = 79 # 1 vertex
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep this as it was -- I know it doesn't really matter, but there are some tricks used to detect the end of paths that are made more obvious when you think of it in hex.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but there are some tricks used to detect the end of paths that are made more obvious when you think of it in hex

For whom? I can see the benefit for developers, but not for everyday users. I'd be happy to add a comment here to jig the readers memory, if that would suffice?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok -- probably simplest to leave it as a (base 10) number, then. A comment here doesn't make sense either.


NUM_VERTICES = [1, 1, 1, 2,
3, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1]
#: A dictionary mapping Path codes to the number of vertices that the
#: code expects.
NUM_VERTICES_FOR_CODE = {STOP: 1,
MOVETO: 1,
LINETO: 1,
CURVE3: 2,
CURVE4: 3,
CLOSEPOLY: 1}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is better as a list -- the lookups are much faster and in this case the speed really matters. Try outputting a large line to the PDF backend for an example.

As an alternative, we could include the old list-based one as a private variable (used internally by iter_segments) and include this dictionary for external use.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I believe this:

In [5]: import random

In [9]: v_dict = {0: 1, 1: 2, 2:1}

In [4]: v_list = [1, 2, 1]

In [10]: %timeit v_dict[random.randint(0, 2)]
100000 loops, best of 3: 2.4 us per loop

In [11]: %timeit v_list[random.randint(0, 2)]
100000 loops, best of 3: 2.39 us per loop

In [12]: %timeit v_list[random.randint(0, 2)]
100000 loops, best of 3: 2.34 us per loop

In [13]: %timeit v_list[random.randint(0, 2)]
100000 loops, best of 3: 2.34 us per loop

In [14]: %timeit v_dict[random.randint(0, 2)]
100000 loops, best of 3: 2.39 us per loop

In [15]: %timeit v_dict[random.randint(0, 2)]
100000 loops, best of 3: 2.38 us per loop

In [16]: %timeit v_dict[random.randint(0, 2)]
100000 loops, best of 3: 2.39 us per loop

Both lookups are O(1) (according to http://wiki.python.org/moin/TimeComplexity). I'm happy enough to revert, but in terms of optimisation I'm really surprised this is noticeable...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting -- that code goes back a ways, so the earlier benchmarking was probably done on Python 2.4 or 2.5, which may behave differently. For dictionaries of this small size, I suppose it doesn't matter -- but note that that table has a worst case time for dictionary lookups of O(n), so it does grow as the dictionary grows.

You've convinced me this is probably ok (and obviously on legibility grounds it's no contest).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but note that that table has a worst case time for dictionary lookups of O(n), so it does grow as the dictionary grows.

Yes - I'm not 100% but I think that is when you have hash collisions.

Ok, I'll keep the dictionary. Thanks for the update.


code_type = np.uint8

Expand All @@ -87,29 +101,31 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, closed=False,
"""
Create a new path with the given vertices and codes.

*vertices* is an Nx2 numpy float array, masked array or Python
sequence.

*codes* is an N-length numpy array or Python sequence of type
:attr:`matplotlib.path.Path.code_type`.

These two arrays must have the same length in the first
dimension.

If *codes* is None, *vertices* will be treated as a series of
line segments.

If *vertices* contains masked values, they will be converted
to NaNs which are then handled correctly by the Agg
PathIterator and other consumers of path data, such as
:meth:`iter_segments`.

*interpolation_steps* is used as a hint to certain projections,
such as Polar, that this path should be linearly interpolated
immediately before drawing. This attribute is primarily an
implementation detail and is not intended for public use.

*readonly*, when True, makes the path immutable.
Parameters
----------
vertices : array_like
The ``(n, 2)`` float array, masked array or sequence of pairs
representing the vertices of the path.

If *vertices* contains masked values, they will be converted
to NaNs which are then handled correctly by the Agg
PathIterator and other consumers of path data, such as
:meth:`iter_segments`.
codes : {None, array_like}, optional
n-length array integers representing the codes of the path.
If not None, codes must be the same length as vertices.
If None, *vertices* will be treated as a series of line segments.
_interpolation_steps : int, optional
Used as a hint to certain projections, such as Polar, that this
path should be linearly interpolated immediately before drawing.
This attribute is primarily an implementation detail and is not
intended for public use.
closed : bool, optional
If *codes* is None and closed is True, vertices will be treated as
line segments of a closed polygon.
readonly : bool, optional
Makes the path behave in an immutable way and sets the vertices
and codes as read-only arrays.
"""
if ma.isMaskedArray(vertices):
vertices = vertices.astype(np.float_).filled(np.nan)
Expand Down Expand Up @@ -144,6 +160,37 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, closed=False,
else:
self._readonly = False

@classmethod
def _fast_from_codes_and_verts(cls, verts, codes, internals=None):
"""
Creates a Path instance without the expense of calling the constructor

Use this method at your own risk...

Parameters
----------
verts : numpy array
codes : numpy array (may not be None)
internals : dict or None
The attributes that the resulting path should have.

"""
internals = internals or {}
pth = cls.__new__(cls)
pth._vertices = verts
pth._codes = codes
pth._readonly = internals.pop('_readonly', False)
pth._should_simplify = internals.pop('_should_simplify', True)
pth._simplify_threshold = internals.pop('_simplify_threshold',
rcParams['path.simplify_threshold'])
pth._has_nonfinite = internals.pop('_has_nonfinite', False)
pth._interpolation_steps = internals.pop('_interpolation_steps', 1)
if internals:
raise ValueError('Unexpected internals provided to '
'_fast_from_codes_and_verts: '
'{0}'.format('\n *'.join(internals.keys())))
return pth

def _update_values(self):
self._should_simplify = (
rcParams['path.simplify'] and
Expand Down Expand Up @@ -246,7 +293,7 @@ def __deepcopy__(self):
@classmethod
def make_compound_path_from_polys(cls, XY):
"""
(static method) Make a compound path object to draw a number
Make a compound path object to draw a number
of polygons with equal numbers of sides XY is a (numpolys x
numsides x 2) numpy array of vertices. Return object is a
:class:`Path`
Expand All @@ -273,10 +320,7 @@ def make_compound_path_from_polys(cls, XY):

@classmethod
def make_compound_path(cls, *args):
"""
(staticmethod) Make a compound path from a list of Path
objects.
"""
"""Make a compound path from a list of Path objects."""
lengths = [len(x) for x in args]
total_length = sum(lengths)

Expand Down Expand Up @@ -313,64 +357,87 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None,
Additionally, this method can provide a number of standard
cleanups and conversions to the path.

*transform*: if not None, the given affine transformation will
be applied to the path.

*remove_nans*: if True, will remove all NaNs from the path and
insert MOVETO commands to skip over them.

*clip*: if not None, must be a four-tuple (x1, y1, x2, y2)
defining a rectangle in which to clip the path.

*snap*: if None, auto-snap to pixels, to reduce
fuzziness of rectilinear lines. If True, force snapping, and
if False, don't snap.

*stroke_width*: the width of the stroke being drawn. Needed
as a hint for the snapping algorithm.

*simplify*: if True, perform simplification, to remove
vertices that do not affect the appearance of the path. If
False, perform no simplification. If None, use the
should_simplify member variable.

*curves*: If True, curve segments will be returned as curve
segments. If False, all curves will be converted to line
segments.

*sketch*: If not None, must be a 3-tuple of the form
(scale, length, randomness), representing the sketch
parameters.
"""
vertices = self.vertices
if not len(vertices):
Parameters
----------
transform : None or :class:`~matplotlib.transforms.Transform` instance
If not None, the given affine transformation will
be applied to the path.
remove_nans : {False, True}, optional
If True, will remove all NaNs from the path and
insert MOVETO commands to skip over them.
clip : None or sequence, optional
If not None, must be a four-tuple (x1, y1, x2, y2)
defining a rectangle in which to clip the path.
snap : None or bool, optional
If None, auto-snap to pixels, to reduce
fuzziness of rectilinear lines. If True, force snapping, and
if False, don't snap.
stroke_width : float, optional
The width of the stroke being drawn. Needed
as a hint for the snapping algorithm.
simplify : None or bool, optional
If True, perform simplification, to remove
vertices that do not affect the appearance of the path. If
False, perform no simplification. If None, use the
should_simplify member variable.
curves : {True, False}, optional
If True, curve segments will be returned as curve
segments. If False, all curves will be converted to line
segments.
sketch : None or sequence, optional
If not None, must be a 3-tuple of the form
(scale, length, randomness), representing the sketch
parameters.
"""
if not len(self):
return

codes = self.codes

NUM_VERTICES = self.NUM_VERTICES
MOVETO = self.MOVETO
LINETO = self.LINETO
CLOSEPOLY = self.CLOSEPOLY
STOP = self.STOP
cleaned = self.cleaned(transform=transform,
remove_nans=remove_nans, clip=clip,
snap=snap, stroke_width=stroke_width,
simplify=simplify, curves=curves,
sketch=sketch)
vertices = cleaned.vertices
codes = cleaned.codes
len_vertices = vertices.shape[0]

vertices, codes = _path.cleanup_path(
self, transform, remove_nans, clip,
snap, stroke_width, simplify, curves,
sketch)
len_vertices = len(vertices)
# Cache these object lookups for performance in the loop.
NUM_VERTICES_FOR_CODE = self.NUM_VERTICES_FOR_CODE
STOP = self.STOP

i = 0
while i < len_vertices:
code = codes[i]
if code == STOP:
return
else:
num_vertices = NUM_VERTICES[int(code) & 0xf]
num_vertices = NUM_VERTICES_FOR_CODE[code]
curr_vertices = vertices[i:i+num_vertices].flatten()
yield curr_vertices, code
i += num_vertices

def cleaned(self, transform=None, remove_nans=False, clip=None,
quantize=False, simplify=False, curves=False,
stroke_width=1.0, snap=False, sketch=None):
"""
Cleans up the path according to the parameters returning a new
Path instance.

.. seealso::

See :meth:`iter_segments` for details of the keyword arguments.

Returns
-------
Path instance with cleaned up vertices and codes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well do this in numpydoc format as well, while we're at it.

"""
vertices, codes = cleanup_path(self, transform,
remove_nans, clip,
snap, stroke_width,
simplify, curves, sketch)
return Path._fast_from_codes_and_verts(vertices, codes)

def transformed(self, transform):
"""
Return a transformed copy of the path.
Expand Down Expand Up @@ -519,7 +586,7 @@ def to_polygons(self, transform=None, width=0, height=0):
@classmethod
def unit_rectangle(cls):
"""
(staticmethod) Returns a :class:`Path` of the unit rectangle
Return a :class:`Path` instance of the unit rectangle
from (0, 0) to (1, 1).
"""
if cls._unit_rectangle is None:
Expand All @@ -534,7 +601,7 @@ def unit_rectangle(cls):
@classmethod
def unit_regular_polygon(cls, numVertices):
"""
(staticmethod) Returns a :class:`Path` for a unit regular
Return a :class:`Path` instance for a unit regular
polygon with the given *numVertices* and radius of 1.0,
centered at (0, 0).
"""
Expand Down Expand Up @@ -563,7 +630,7 @@ def unit_regular_polygon(cls, numVertices):
@classmethod
def unit_regular_star(cls, numVertices, innerCircle=0.5):
"""
(staticmethod) Returns a :class:`Path` for a unit regular star
Return a :class:`Path` for a unit regular star
with the given numVertices and radius of 1.0, centered at (0,
0).
"""
Expand Down Expand Up @@ -592,7 +659,7 @@ def unit_regular_star(cls, numVertices, innerCircle=0.5):
@classmethod
def unit_regular_asterisk(cls, numVertices):
"""
(staticmethod) Returns a :class:`Path` for a unit regular
Return a :class:`Path` for a unit regular
asterisk with the given numVertices and radius of 1.0,
centered at (0, 0).
"""
Expand All @@ -603,7 +670,7 @@ def unit_regular_asterisk(cls, numVertices):
@classmethod
def unit_circle(cls):
"""
(staticmethod) Returns a :class:`Path` of the unit circle.
Return a :class:`Path` of the unit circle.
The circle is approximated using cubic Bezier curves. This
uses 8 splines around the circle using the approach presented
here:
Expand Down Expand Up @@ -666,7 +733,7 @@ def unit_circle(cls):
@classmethod
def unit_circle_righthalf(cls):
"""
(staticmethod) Returns a :class:`Path` of the right half
Return a :class:`Path` of the right half
of a unit circle. The circle is approximated using cubic Bezier
curves. This uses 4 splines around the circle using the approach
presented here:
Expand Down Expand Up @@ -712,7 +779,7 @@ def unit_circle_righthalf(cls):
@classmethod
def arc(cls, theta1, theta2, n=None, is_wedge=False):
"""
(staticmethod) Returns an arc on the unit circle from angle
Return an arc on the unit circle from angle
*theta1* to angle *theta2* (in degrees).

If *n* is provided, it is the number of spline segments to make.
Expand Down Expand Up @@ -790,7 +857,7 @@ def arc(cls, theta1, theta2, n=None, is_wedge=False):
@classmethod
def wedge(cls, theta1, theta2, n=None):
"""
(staticmethod) Returns a wedge of the unit circle from angle
Return a wedge of the unit circle from angle
*theta1* to angle *theta2* (in degrees).

If *n* is provided, it is the number of spline segments to make.
Expand Down
0