Skip to content

Commit

Permalink
Merge pull request #1329 from mdboom/wiggles
Browse files Browse the repository at this point in the history
Add a "sketch" path filter for XKCD style graphics.
  • Loading branch information
pelson committed May 14, 2013
2 parents 47193ee + 4a61f50 commit 6b442b8
Show file tree
Hide file tree
Showing 37 changed files with 2,372 additions and 123 deletions.
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
7 changes: 6 additions & 1 deletion doc/api/api_changes.rst
Expand Up @@ -15,7 +15,12 @@ For new features that were added to matplotlib, please see
Changes in 1.3.x
================

* Fixed a bug in setting the position for the right/top spine with data
* The `font.*` rcParams now affect only text objects created after the
rcParam has been set, and will not retroactively affect already
existing text objects. This brings their behavior in line with most
other rcParams.

* 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
28 changes: 26 additions & 2 deletions doc/users/whats_new.rst
Expand Up @@ -21,6 +21,30 @@ revision, see the :ref:`github-stats`.

new in matplotlib-1.3
=====================
`xkcd`-style sketch plotting
----------------------------

To give your plots a sense of authority that they may be missing,
Michael Droettboom (inspired by the work of many others in
:ghpull:`1329`) has added an `xkcd-style <http://xkcd.com/>`_ sketch
plotting mode. To use it, simply call `pyplot.xkcd` before creating
your plot.

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

Path effects on lines
---------------------
Thanks to Jae-Joon Lee, path effects now also work on plot lines.

.. plot:: mpl_examples/pylab_examples/patheffect_demo.py

Changes to font rcParams
------------------------
The `font.*` rcParams now affect only text objects created after the
rcParam has been set, and will not retroactively affect already
existing text objects. This brings their behavior in line with most
other rcParams.

``axes.xmargin`` and ``axes.ymargin`` added to rcParams
-------------------------------------------------------
``rcParam`` values (``axes.xmargin`` and ``axes.ymargin``) were added
Expand Down Expand Up @@ -148,8 +172,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
24 changes: 16 additions & 8 deletions examples/pylab_examples/patheffect_demo.py
Expand Up @@ -13,19 +13,28 @@

txt.set_path_effects([PathEffects.withStroke(linewidth=3,
foreground="w")])
txt.arrow_patch.set_path_effects([PathEffects.Stroke(linewidth=5,
foreground="w"),
PathEffects.Normal()])
txt.arrow_patch.set_path_effects([
PathEffects.Stroke(linewidth=5, 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")
clbls = ax2.clabel(cntr, fmt="%2.0f", use_clabeltext=True)
plt.setp(clbls,
path_effects=[PathEffects.withStroke(linewidth=3,
foreground="w")])

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

clbls = ax2.clabel(cntr, fmt="%2.0f", use_clabeltext=True)
plt.setp(clbls, path_effects=[
PathEffects.withStroke(linewidth=3, foreground="w")])

# shadow as a path effect
ax3 = plt.subplot(133)
Expand All @@ -34,4 +43,3 @@
leg.legendPatch.set_path_effects([PathEffects.withSimplePatchShadow()])

plt.show()

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
4 changes: 3 additions & 1 deletion examples/pylab_examples/to_numeric.py 100644 → 100755
Expand Up @@ -30,5 +30,7 @@
X.shape = h, w, 3

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

# Uncomment this line to display the image using ImageMagick's
# `display` tool.
# im.show()
54 changes: 54 additions & 0 deletions examples/showcase/xkcd.py
@@ -0,0 +1,54 @@
import matplotlib.pyplot as plt
import numpy as np

with plt.xkcd():
# Based on "Stove Ownership" from XKCD by Randall Monroe
# http://xkcd.com/418/

fig = plt.figure()
ax = fig.add_axes((0.1, 0.2, 0.8, 0.7))
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.text(
0.5, 0.05,
'"Stove Ownership" from xkcd by Randall Monroe',
ha='center')

# Based on "The Data So Far" from XKCD by Randall Monroe
# http://xkcd.com/373/

fig = plt.figure()
ax = fig.add_axes((0.1, 0.2, 0.8, 0.7))
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")

fig.text(
0.5, 0.05,
'"The Data So Far" from xkcd by Randall Monroe',
ha='center')

plt.show()
6 changes: 5 additions & 1 deletion lib/matplotlib/__init__.py
Expand Up @@ -1052,12 +1052,15 @@ class rc_context(object):
def __init__(self, rc=None, fname=None):
self.rcdict = rc
self.fname = fname
def __enter__(self):
self._rcparams = rcParams.copy()
if self.fname:
rc_file(self.fname)
if self.rcdict:
rcParams.update(self.rcdict)

def __enter__(self):
return self

def __exit__(self, type, value, tb):
rcParams.update(self._rcparams)

Expand Down Expand Up @@ -1190,6 +1193,7 @@ def tk_window_focus():
'matplotlib.tests.test_mathtext',
'matplotlib.tests.test_mlab',
'matplotlib.tests.test_patches',
'matplotlib.tests.test_patheffects',
'matplotlib.tests.test_pickle',
'matplotlib.tests.test_rcparams',
'matplotlib.tests.test_scale',
Expand Down
63 changes: 62 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,63 @@ def set_snap(self, snap):
"""
self._snap = snap

def get_sketch_params(self):
"""
Returns the sketch parameters for the artist.
Returns
-------
sketch_params : tuple or `None`
A 3-tuple with the following 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.
Parameters
----------
scale : float, optional
The amplitude of the wiggle perpendicular to the source
line, in pixels. If scale is `None`, or not provided, no
sketch filter will be provided.
length : float, optional
The length of the wiggle along the line, in pixels
(default 128.0)
randomness : float, optional
The scale factor by which the length is shrunken or
expanded (default 16.0)
"""
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 +731,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 +787,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
53 changes: 51 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,53 @@ def get_hatch_path(self, density=6.0):
return None
return Path.hatch(self._hatch, density)

def get_sketch_params(self):
"""
Returns the sketch parameters for the artist.
Returns
-------
sketch_params : tuple or `None`
A 3-tuple with the following 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.
Parameters
----------
scale : float, optional
The amplitude of the wiggle perpendicular to the source
line, in pixels. If scale is `None`, or not provided, no
sketch filter will be provided.
length : float, optional
The length of the wiggle along the line, in pixels
(default 128.0)
randomness : float, optional
The scale factor by which the length is shrunken or
expanded (default 16.0)
"""
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 +1986,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 +2006,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

0 comments on commit 6b442b8

Please sign in to comment.