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

Add a "sketch" path filter #1329

Merged
merged 13 commits into from May 14, 2013
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG
Expand Up @@ -16,6 +16,8 @@
2013-04-15 Added 'axes.xmargin' and 'axes.ymargin' to rpParams to set default
margins on auto-scaleing. - TAC

2013-04-16 Added patheffect support for Line2D objects. -JJL

2013-03-19 Added support for passing `linestyle` kwarg to `step` so all `plot`
kwargs are passed to the underlying `plot` call. -TAC

Expand Down
16 changes: 14 additions & 2 deletions doc/users/whats_new.rst
Expand Up @@ -21,6 +21,18 @@ revision, see the :ref:`github-stats`.

new in matplotlib-1.3
Copy link
Member

Choose a reason for hiding this comment

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

Path effects on lines deserves to go in here too.

=====================
`xkcd`-style sketch plotting
----------------------------

To gives your plots a sense of authority that they may be missing,
Copy link
Member

Choose a reason for hiding this comment

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

"gives" typo

Michael Droettboom (inspired by the work of many others in `issue
#1329 <https://github.com/matplotlib/matplotlib/pull/1329>`_) has
Copy link
Member

Choose a reason for hiding this comment

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

We have a role to make the linking easier (ghissue I think) - see doc/sphinxext/github.py

added an `xkcd-style <xkcd.com>`_ sketch plotting mode. To use it,
Copy link
Member

Choose a reason for hiding this comment

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

This link is local - so results in a 404.

simply call `pyplot.xkcd` before creating your plot.

.. plot:: mpl_examples/showcase/xkcd.py


``axes.xmargin`` and ``axes.ymargin`` added to rcParams
-------------------------------------------------------
``rcParam`` values (``axes.xmargin`` and ``axes.ymargin``) were added
Expand Down Expand Up @@ -142,8 +154,8 @@ the bottom of the text bounding box.

``savefig.jpeg_quality`` added to rcParams
------------------------------------------------------------------------------
``rcParam`` value ``savefig.jpeg_quality`` was added so that the user can
configure the default quality used when a figure is written as a JPEG. The
``rcParam`` value ``savefig.jpeg_quality`` was added so that the user can
configure the default quality used when a figure is written as a JPEG. The
default quality is 95; previously, the default quality was 75. This change
minimizes the artifacting inherent in JPEG images, particularly with images
that have sharp changes in color as plots often do.
Expand Down
13 changes: 13 additions & 0 deletions examples/pylab_examples/patheffect_demo.py
Expand Up @@ -17,10 +17,23 @@
foreground="w"),
PathEffects.Normal()])

ax1.grid(True, linestyle="-")

pe = [PathEffects.withStroke(linewidth=3,
foreground="w")]
for l in ax1.get_xgridlines() + ax1.get_ygridlines():
l.set_path_effects(pe)

ax2 = plt.subplot(132)
arr = np.arange(25).reshape((5,5))
ax2.imshow(arr)
cntr = ax2.contour(arr, colors="k")

plt.setp(cntr.collections,
path_effects=[PathEffects.withStroke(linewidth=3,
foreground="w")])

Copy link
Member

Choose a reason for hiding this comment

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

Picky: too many newlines


clbls = ax2.clabel(cntr, fmt="%2.0f", use_clabeltext=True)
plt.setp(clbls,
path_effects=[PathEffects.withStroke(linewidth=3,
Expand Down
2 changes: 1 addition & 1 deletion examples/pylab_examples/simple_plot.py
Expand Up @@ -2,7 +2,7 @@

t = arange(0.0, 2.0, 0.01)
s = sin(2*pi*t)
plot(t, s, linewidth=1.0)
plot(t, s)

xlabel('time (s)')
ylabel('voltage (mV)')
Expand Down
3 changes: 1 addition & 2 deletions examples/pylab_examples/to_numeric.py 100644 → 100755
Expand Up @@ -30,5 +30,4 @@
X.shape = h, w, 3

im = Image.fromstring( "RGB", (w,h), s)
im.show()

# im.show()
Copy link
Member

Choose a reason for hiding this comment

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

Intentional?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes -- though I should probably remove the line altogether -- this causes ImageMagick's "display" utility to be called during the documentation build.

Copy link
Member

Choose a reason for hiding this comment

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

Oh! That annoys me everytime and I've never got to the bottom of it! 😄

40 changes: 40 additions & 0 deletions examples/showcase/xkcd.py
@@ -0,0 +1,40 @@
from matplotlib import pyplot as plt
Copy link
Member

Choose a reason for hiding this comment

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

It'd be nice to reference the XKCD chart that is being reproduced.

Copy link
Member

Choose a reason for hiding this comment

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

Picky: its fewer characters (and more consistent?) to type import matplotlib.pyplot as plt

import numpy as np

plt.xkcd()

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.xticks([])
plt.yticks([])
ax.set_ylim([-30, 10])

data = np.ones(100)
data[70:] -= np.arange(30)

plt.annotate(
'THE DAY I REALIZED\nI COULD COOK BACON\nWHENEVER I WANTED',
xy=(70, 1), arrowprops=dict(arrowstyle='->'), xytext=(15, -10))

plt.plot(data)

plt.xlabel('time')
plt.ylabel('my overall health')

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.bar([-0.125, 1.0-0.125], [0, 100], 0.25)
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.set_xticks([0, 1])
ax.set_xlim([-0.5, 1.5])
ax.set_ylim([0, 110])
ax.set_xticklabels(['CONFIRMED BY\nEXPERIMENT', 'REFUTED BY\nEXPERIMENT'])
plt.yticks([])

plt.title("CLAIMS OF SUPERNATURAL POWERS")

plt.show()
52 changes: 51 additions & 1 deletion lib/matplotlib/artist.py
Expand Up @@ -101,6 +101,8 @@ def __init__(self):
self._url = None
self._gid = None
self._snap = None
self._sketch = rcParams['path.sketch']
self._path_effects = rcParams['path.effects']

def __getstate__(self):
d = self.__dict__.copy()
Expand Down Expand Up @@ -456,6 +458,52 @@ def set_snap(self, snap):
"""
self._snap = snap

def get_sketch_params(self):
Copy link
Member

Choose a reason for hiding this comment

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

I'd be keen to use numpydoc form here (https://github.com/matplotlib/matplotlib/wiki/Mep10)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, sure -- I didn't notice that. This PR predates MEP10, so I forgot to update it.

"""
Returns the sketch parameters, which is a tuple with three elements:

* *scale*: The amplitude of the wiggle perpendicular to the
source line.

* *length*: The length of the wiggle along the line.

* *randomness*: The scale factor by which the length is
shrunken or expanded.

May return `None` if no sketch parameters were set.
"""
return self._sketch

def set_sketch_params(self, scale=None, length=None, randomness=None):
"""
Sets the the sketch parameters:

* *scale*: The amplitude of the wiggle perpendicular to the
source line, in pixels.

* *length*: The length of the wiggle along the line, in
pixels (default 128.0)

* *randomness*: The scale factor by which the length is
shrunken or expanded (default 16.0)

If *scale* is None, no wiggling will be set.
"""
if scale is None:
self._sketch = None
else:
self._sketch = (scale, length or 128.0, randomness or 16.0)

def set_path_effects(self, path_effects):
"""
set path_effects, which should be a list of instances of
matplotlib.patheffect._Base class or its derivatives.
"""
self._path_effects = path_effects

def get_path_effects(self):
return self._path_effects

def get_figure(self):
"""
Return the :class:`~matplotlib.figure.Figure` instance the
Expand Down Expand Up @@ -672,7 +720,7 @@ def update(self, props):
store = self.eventson
self.eventson = False
changed = False

for k, v in props.iteritems():
func = getattr(self, 'set_' + k, None)
if func is None or not callable(func):
Expand Down Expand Up @@ -728,6 +776,8 @@ def update_from(self, other):
self._clippath = other._clippath
self._lod = other._lod
self._label = other._label
self._sketch = other._sketch
self._path_effects = other._path_effects
self.pchanged()

def properties(self):
Expand Down
42 changes: 40 additions & 2 deletions lib/matplotlib/backend_bases.py
Expand Up @@ -701,6 +701,7 @@ def __init__(self):
self._url = None
self._gid = None
self._snap = None
self._sketch = None

def copy_properties(self, gc):
'Copy properties from gc to self'
Expand All @@ -720,6 +721,7 @@ def copy_properties(self, gc):
self._url = gc._url
self._gid = gc._gid
self._snap = gc._snap
self._sketch = gc._sketch

def restore(self):
"""
Expand Down Expand Up @@ -1003,6 +1005,42 @@ def get_hatch_path(self, density=6.0):
return None
return Path.hatch(self._hatch, density)

def get_sketch_params(self):
"""
Copy link
Member

Choose a reason for hiding this comment

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

Not an action: We should think about removing the duplicated docstings (duplicated in Artist) from here.

Returns the sketch parameters, which is a tuple with three elements:

* *scale*: The amplitude of the wiggle perpendicular to the
source line.

* *length*: The length of the wiggle along the line.

* *randomness*: The scale factor by which the length is
shrunken or expanded.

May return `None` if no sketch parameters were set.
"""
return self._sketch

def set_sketch_params(self, scale=None, length=None, randomness=None):
Copy link
Member

Choose a reason for hiding this comment

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

Technically I don't think these need to be keywords as they are always given (in the draw method). But I agree that keeping the signature consistent is useful.

"""
Sets the the sketch parameters:

* *scale*: The amplitude of the wiggle perpendicular to the
source line.

* *length*: The length of the wiggle along the line, in
pixels.

* *randomness*: The scale factor by which the length is
shrunken or expanded, in pixels.

If *scale* is None, no wiggling will be set.
"""
if scale is None:
self._sketch = None
else:
self._sketch = (scale, length or 128.0, randomness or 16.0)


class TimerBase(object):
'''
Expand Down Expand Up @@ -1937,7 +1975,7 @@ def print_jpg(self, filename_or_obj, *args, **kwargs):

*quality*: The image quality, on a scale from 1 (worst) to
95 (best). The default is 95, if not given in the
matplotlibrc file in the savefig.jpeg_quality parameter.
matplotlibrc file in the savefig.jpeg_quality parameter.
Values above 95 should be avoided; 100 completely
disables the JPEG quantization stage.

Expand All @@ -1957,7 +1995,7 @@ def print_jpg(self, filename_or_obj, *args, **kwargs):
options = cbook.restrict_dict(kwargs, ['quality', 'optimize',
'progressive'])

if 'quality' not in options:
if 'quality' not in options:
options['quality'] = rcParams['savefig.jpeg_quality']

return image.save(filename_or_obj, format='jpeg', **options)
Expand Down
13 changes: 8 additions & 5 deletions lib/matplotlib/backends/backend_pdf.py
Expand Up @@ -1313,11 +1313,12 @@ def writePathCollectionTemplates(self):
self.endStream()

@staticmethod
def pathOperations(path, transform, clip=None, simplify=None):
def pathOperations(path, transform, clip=None, simplify=None, sketch=None):
cmds = []
last_points = None
for points, code in path.iter_segments(transform, clip=clip,
simplify=simplify):
simplify=simplify,
sketch=sketch):
if code == Path.MOVETO:
# This is allowed anywhere in the path
cmds.extend(points)
Expand All @@ -1340,14 +1341,15 @@ def pathOperations(path, transform, clip=None, simplify=None):
last_points = points
return cmds

def writePath(self, path, transform, clip=False):
def writePath(self, path, transform, clip=False, sketch=None):
if clip:
clip = (0.0, 0.0, self.width * 72, self.height * 72)
simplify = path.should_simplify
else:
clip = None
simplify = False
cmds = self.pathOperations(path, transform, clip, simplify=simplify)
cmds = self.pathOperations(path, transform, clip, simplify=simplify,
sketch=sketch)
self.output(*cmds)

def reserveObject(self, name=''):
Expand Down Expand Up @@ -1526,7 +1528,8 @@ def draw_path(self, gc, path, transform, rgbFace=None):
self.check_gc(gc, rgbFace)
self.file.writePath(
path, transform,
rgbFace is None and gc.get_hatch_path() is None)
rgbFace is None and gc.get_hatch_path() is None,
gc.get_sketch_params())
self.file.output(self.gc.paint())

def draw_path_collection(self, gc, master_transform, paths, all_transforms,
Expand Down
26 changes: 21 additions & 5 deletions lib/matplotlib/collections.py
Expand Up @@ -91,6 +91,7 @@ def __init__(self,
urls=None,
offset_position='screen',
zorder=1,
path_effects=None,
**kwargs
):
"""
Expand Down Expand Up @@ -123,6 +124,7 @@ def __init__(self,
else:
self._uniform_offsets = offsets

self._path_effects = None
self.update(kwargs)
self._paths = None

Expand Down Expand Up @@ -258,11 +260,24 @@ def draw(self, renderer):
if self._hatch:
gc.set_hatch(self._hatch)

renderer.draw_path_collection(
gc, transform.frozen(), paths, self.get_transforms(),
offsets, transOffset, self.get_facecolor(), self.get_edgecolor(),
self._linewidths, self._linestyles, self._antialiaseds, self._urls,
self._offset_position)
if self.get_sketch_params() is not None:
gc.set_sketch_params(*self.get_sketch_params())

if self.get_path_effects():
#from patheffects import PathEffectsRenderer
Copy link
Member

Choose a reason for hiding this comment

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

Rogue comment.

for pe in self.get_path_effects():
pe.draw_path_collection(renderer,
gc, transform.frozen(), paths, self.get_transforms(),
offsets, transOffset, self.get_facecolor(), self.get_edgecolor(),
self._linewidths, self._linestyles, self._antialiaseds, self._urls,
self._offset_position)
else:

Copy link
Member

Choose a reason for hiding this comment

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

Unnecessary newline.

renderer.draw_path_collection(
gc, transform.frozen(), paths, self.get_transforms(),
offsets, transOffset, self.get_facecolor(), self.get_edgecolor(),
self._linewidths, self._linestyles, self._antialiaseds, self._urls,
self._offset_position)

gc.restore()
renderer.close_group(self.__class__.__name__)
Expand Down Expand Up @@ -642,6 +657,7 @@ def update_from(self, other):
self.cmap = other.cmap
# self.update_dict = other.update_dict # do we need to copy this? -JJL


# these are not available for the object inspector until after the
# class is built so we define an initial set here for the init
# function and they will be overridden after object defn
Expand Down