Skip to content

Commit c288df2

Browse files
committed
Add xkcd() function to set up a number of parameters to create plots that look like xkcd. Add an example, and a what's new. Expose path effects as an rcParam so that they can be set globally.
1 parent 65544a1 commit c288df2

File tree

14 files changed

+125
-61
lines changed

14 files changed

+125
-61
lines changed

doc/users/whats_new.rst

+14-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ revision, see the :ref:`github-stats`.
2121

2222
new in matplotlib-1.3
2323
=====================
24+
`xkcd`-style sketch plotting
25+
----------------------------
26+
27+
To gives your plots a sense of authority that they may be missing,
28+
Michael Droettboom (inspired by the work of many others in `issue
29+
#1329 <https://github.com/matplotlib/matplotlib/pull/1329>`_) has
30+
added an `xkcd-style <xkcd.com>`_ sketch plotting mode. To use it,
31+
simply call `pyplot.xkcd` before creating your plot.
32+
33+
.. plot:: mpl_examples/showcase/xkcd.py
34+
35+
2436
``axes.xmargin`` and ``axes.ymargin`` added to rcParams
2537
-------------------------------------------------------
2638
``rcParam`` values (``axes.xmargin`` and ``axes.ymargin``) were added
@@ -142,8 +154,8 @@ the bottom of the text bounding box.
142154

143155
``savefig.jpeg_quality`` added to rcParams
144156
------------------------------------------------------------------------------
145-
``rcParam`` value ``savefig.jpeg_quality`` was added so that the user can
146-
configure the default quality used when a figure is written as a JPEG. The
157+
``rcParam`` value ``savefig.jpeg_quality`` was added so that the user can
158+
configure the default quality used when a figure is written as a JPEG. The
147159
default quality is 95; previously, the default quality was 75. This change
148160
minimizes the artifacting inherent in JPEG images, particularly with images
149161
that have sharp changes in color as plots often do.

examples/pylab_examples/simple_plot.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
t = arange(0.0, 2.0, 0.01)
44
s = sin(2*pi*t)
5-
plot(t, s, linewidth=1.0)
5+
plot(t, s)
66

77
xlabel('time (s)')
88
ylabel('voltage (mV)')

examples/pylab_examples/to_numeric.py

100644100755
+1-2
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,4 @@
3030
X.shape = h, w, 3
3131

3232
im = Image.fromstring( "RGB", (w,h), s)
33-
im.show()
34-
33+
# im.show()

examples/showcase/xkcd.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from matplotlib import pyplot as plt
2+
import numpy as np
3+
4+
plt.xkcd()
5+
6+
fig = plt.figure()
7+
ax = fig.add_subplot(1, 1, 1)
8+
ax.spines['right'].set_color('none')
9+
ax.spines['top'].set_color('none')
10+
plt.xticks([])
11+
plt.yticks([])
12+
ax.set_ylim([-30, 10])
13+
14+
data = np.ones(100)
15+
data[70:] -= np.arange(30)
16+
17+
plt.annotate(
18+
'THE DAY I REALIZED\nI COULD COOK BACON\nWHENEVER I WANTED',
19+
xy=(70, 1), arrowprops=dict(arrowstyle='->'), xytext=(15, -10))
20+
21+
plt.plot(data)
22+
23+
plt.xlabel('time')
24+
plt.ylabel('my overall health')
25+
26+
fig = plt.figure()
27+
ax = fig.add_subplot(1, 1, 1)
28+
ax.bar([-0.125, 1.0-0.125], [0, 100], 0.25)
29+
ax.spines['right'].set_color('none')
30+
ax.spines['top'].set_color('none')
31+
ax.xaxis.set_ticks_position('bottom')
32+
ax.set_xticks([0, 1])
33+
ax.set_xlim([-0.5, 1.5])
34+
ax.set_ylim([0, 110])
35+
ax.set_xticklabels(['CONFIRMED BY\nEXPERIMENT', 'REFUTED BY\nEXPERIMENT'])
36+
plt.yticks([])
37+
38+
plt.title("CLAIMS OF SUPERNATURAL POWERS")
39+
40+
plt.show()

lib/matplotlib/artist.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def __init__(self):
102102
self._gid = None
103103
self._snap = None
104104
self._sketch = rcParams['path.sketch']
105+
self._path_effects = rcParams['path.effects']
105106

106107
def __getstate__(self):
107108
d = self.__dict__.copy()
@@ -493,6 +494,16 @@ def set_sketch_params(self, scale=None, length=None, randomness=None):
493494
else:
494495
self._sketch = (scale, length or 128.0, randomness or 16.0)
495496

497+
def set_path_effects(self, path_effects):
498+
"""
499+
set path_effects, which should be a list of instances of
500+
matplotlib.patheffect._Base class or its derivatives.
501+
"""
502+
self._path_effects = path_effects
503+
504+
def get_path_effects(self):
505+
return self._path_effects
506+
496507
def get_figure(self):
497508
"""
498509
Return the :class:`~matplotlib.figure.Figure` instance the
@@ -709,7 +720,7 @@ def update(self, props):
709720
store = self.eventson
710721
self.eventson = False
711722
changed = False
712-
723+
713724
for k, v in props.iteritems():
714725
func = getattr(self, 'set_' + k, None)
715726
if func is None or not callable(func):
@@ -765,6 +776,8 @@ def update_from(self, other):
765776
self._clippath = other._clippath
766777
self._lod = other._lod
767778
self._label = other._label
779+
self._sketch = other._sketch
780+
self._path_effects = other._path_effects
768781
self.pchanged()
769782

770783
def properties(self):

lib/matplotlib/backend_bases.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,7 @@ def copy_properties(self, gc):
721721
self._url = gc._url
722722
self._gid = gc._gid
723723
self._snap = gc._snap
724+
self._sketch = gc._sketch
724725

725726
def restore(self):
726727
"""
@@ -1974,7 +1975,7 @@ def print_jpg(self, filename_or_obj, *args, **kwargs):
19741975
19751976
*quality*: The image quality, on a scale from 1 (worst) to
19761977
95 (best). The default is 95, if not given in the
1977-
matplotlibrc file in the savefig.jpeg_quality parameter.
1978+
matplotlibrc file in the savefig.jpeg_quality parameter.
19781979
Values above 95 should be avoided; 100 completely
19791980
disables the JPEG quantization stage.
19801981
@@ -1994,7 +1995,7 @@ def print_jpg(self, filename_or_obj, *args, **kwargs):
19941995
options = cbook.restrict_dict(kwargs, ['quality', 'optimize',
19951996
'progressive'])
19961997

1997-
if 'quality' not in options:
1998+
if 'quality' not in options:
19981999
options['quality'] = rcParams['savefig.jpeg_quality']
19992000

20002001
return image.save(filename_or_obj, format='jpeg', **options)

lib/matplotlib/collections.py

-10
Original file line numberDiff line numberDiff line change
@@ -657,16 +657,6 @@ def update_from(self, other):
657657
self.cmap = other.cmap
658658
# self.update_dict = other.update_dict # do we need to copy this? -JJL
659659

660-
def set_path_effects(self, path_effects):
661-
"""
662-
set path_effects, which should be a list of instances of
663-
matplotlib.patheffect._Base class or its derivatives.
664-
"""
665-
self._path_effects = path_effects
666-
667-
def get_path_effects(self):
668-
return self._path_effects
669-
670660

671661
# these are not available for the object inspector until after the
672662
# class is built so we define an initial set here for the init

lib/matplotlib/lines.py

+1-13
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,6 @@ def __init__(self, xdata, ydata,
237237
self._invalidy = True
238238
self.set_data(xdata, ydata)
239239

240-
self.set_path_effects(path_effects)
241-
242240
def contains(self, mouseevent):
243241
"""
244242
Test whether the mouse event occurred on the line. The pick
@@ -556,7 +554,7 @@ def draw(self, renderer):
556554
funcname = self.drawStyles.get(self._drawstyle, '_draw_lines')
557555
drawFunc = getattr(self, funcname)
558556

559-
if self.get_path_effects():
557+
if self.get_path_effects() and self._linewidth:
560558
affine_frozen = affine.frozen()
561559
for pe in self.get_path_effects():
562560
pe_renderer = pe.get_proxy_renderer(renderer)
@@ -1189,16 +1187,6 @@ def is_dashed(self):
11891187
'return True if line is dashstyle'
11901188
return self._linestyle in ('--', '-.', ':')
11911189

1192-
def set_path_effects(self, path_effects):
1193-
"""
1194-
set path_effects, which should be a list of instances of
1195-
matplotlib.patheffect._Base class or its derivatives.
1196-
"""
1197-
self._path_effects = path_effects
1198-
1199-
def get_path_effects(self):
1200-
return self._path_effects
1201-
12021190

12031191
class VertexSelector:
12041192
"""

lib/matplotlib/patches.py

+3-14
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,6 @@ def __init__(self,
9898
self.set_fill(fill)
9999
self._combined_transform = transforms.IdentityTransform()
100100

101-
self.set_path_effects(path_effects)
102-
103101
if len(kwargs):
104102
artist.setp(self, **kwargs)
105103

@@ -385,16 +383,6 @@ def get_hatch(self):
385383
'Return the current hatching pattern'
386384
return self._hatch
387385

388-
def set_path_effects(self, path_effects):
389-
"""
390-
set path_effects, which should be a list of instances of
391-
matplotlib.patheffect._Base class or its derivatives.
392-
"""
393-
self._path_effects = path_effects
394-
395-
def get_path_effects(self):
396-
return self._path_effects
397-
398386
@allow_rasterization
399387
def draw(self, renderer):
400388
'Draw the :class:`Patch` to the given *renderer*.'
@@ -435,8 +423,9 @@ def draw(self, renderer):
435423
affine = transform.get_affine()
436424

437425
if self.get_path_effects():
438-
for path_effect in self.get_path_effects():
439-
path_effect.draw_path(renderer, gc, tpath, affine, rgbFace)
426+
if gc.get_linewidth() or rgbFace is not None:
427+
for path_effect in self.get_path_effects():
428+
path_effect.draw_path(renderer, gc, tpath, affine, rgbFace)
440429
else:
441430
renderer.draw_path(gc, tpath, affine, rgbFace)
442431

lib/matplotlib/pyplot.py

+38-6
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,38 @@ def setp(*args, **kwargs):
255255
draw_if_interactive()
256256
return ret
257257

258+
def xkcd():
259+
"""
260+
Turns on `xkcd <xkcd.com>`_ sketch-style drawing mode. This will
261+
only have effect on things drawn after this function is called.
262+
263+
For best results, the "Humor Sans" font should be installed: it is
264+
not included with matplotlib.
265+
266+
This function works by setting a whole slew of rcParams, so it will
267+
probably override others you have set before.
268+
"""
269+
from matplotlib import patheffects
270+
rcParams['text.usetex'] = False
271+
rcParams['font.family'] = 'fantasy'
272+
rcParams['font.fantasy'] = ['Humor Sans', 'Comic Sans MS']
273+
rcParams['font.size'] = 14.0
274+
rcParams['path.sketch'] = (1, 100, 2)
275+
rcParams['path.effects'] = [
276+
patheffects.withStroke(linewidth=4, foreground="w")]
277+
rcParams['axes.linewidth'] = 1.5
278+
rcParams['lines.linewidth'] = 2.0
279+
rcParams['figure.facecolor'] = 'white'
280+
rcParams['grid.linewidth'] = 0.0
281+
rcParams['axes.unicode_minus'] = False
282+
rcParams['axes.color_cycle'] = ['b', 'r', 'c', 'm']
283+
# rcParams['axes.clip'] = False
284+
rcParams['xtick.major.size'] = 8
285+
rcParams['xtick.major.width'] = 3
286+
rcParams['ytick.major.size'] = 8
287+
rcParams['ytick.major.width'] = 3
288+
289+
258290
## Figures ##
259291

260292

@@ -1790,7 +1822,7 @@ def colormaps():
17901822
for nominal data that has no inherent ordering, where color is used
17911823
only to distinguish categories
17921824
1793-
The base colormaps are derived from those of the same name provided
1825+
The base colormaps are derived from those of the same name provided
17941826
with Matlab:
17951827
17961828
========= =======================================================
@@ -1942,14 +1974,14 @@ def colormaps():
19421974
rainbow spectral purple-blue-green-yellow-orange-red colormap
19431975
with diverging luminance
19441976
seismic diverging blue-white-red
1945-
nipy_spectral black-purple-blue-green-yellow-red-white spectrum,
1977+
nipy_spectral black-purple-blue-green-yellow-red-white spectrum,
19461978
originally from the Neuroimaging in Python project
19471979
terrain mapmaker's colors, blue-green-yellow-brown-white,
19481980
originally from IGOR Pro
19491981
============= =======================================================
19501982
19511983
The following colormaps are redundant and may be removed in future
1952-
versions. It's recommended to use the names in the descriptions
1984+
versions. It's recommended to use the names in the descriptions
19531985
instead, which produce identical output:
19541986
19551987
========= =======================================================
@@ -1980,11 +2012,11 @@ def colormaps():
19802012
Color-Scale Images
19812013
<http://www.mathworks.com/matlabcentral/fileexchange/2662-cmrmap-m>`_
19822014
by Carey Rappaport
1983-
2015+
19842016
.. [#] Changed to distinguish from ColorBrewer's *Spectral* map.
1985-
:func:`spectral` still works, but
2017+
:func:`spectral` still works, but
19862018
``set_cmap('nipy_spectral')`` is recommended for clarity.
1987-
2019+
19882020
19892021
"""
19902022
return sorted(cm.cmap_d.keys())

lib/matplotlib/rcsetup.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ def __call__(self, s):
5757
% (self.key, s, self.valid.values()))
5858

5959

60+
def validate_any(s):
61+
return s
62+
63+
6064
def validate_path_exists(s):
6165
"""If s is a path, return s, else False"""
6266
if os.path.exists(s):
@@ -750,10 +754,10 @@ def __call__(self, s):
750754
'path.simplify': [True, validate_bool],
751755
'path.simplify_threshold': [1.0 / 9.0, ValidateInterval(0.0, 1.0)],
752756
'path.snap': [True, validate_bool],
753-
'agg.path.chunksize': [0, validate_int], # 0 to disable chunking;
754757
'path.sketch': [None, validate_sketch],
755-
# recommend about 20000 to
756-
# enable. Experimental.
758+
'path.effects': [[], validate_any],
759+
'agg.path.chunksize': [0, validate_int], # 0 to disable chunking;
760+
757761
# key-mappings (multi-character mappings should be a list/tuple)
758762
'keymap.fullscreen': [('f', 'ctrl+f'), validate_stringlist],
759763
'keymap.home': [['h', 'r', 'home'], validate_stringlist],

lib/matplotlib/text.py

-7
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ def __init__(self,
179179
elif is_string_like(fontproperties):
180180
fontproperties = FontProperties(fontproperties)
181181

182-
self.set_path_effects(path_effects)
183182
self.set_text(text)
184183
self.set_color(color)
185184
self._verticalalignment = verticalalignment
@@ -449,12 +448,6 @@ def get_text_width_height_descent(*kl, **kwargs):
449448
self.cached[key] = ret
450449
return ret
451450

452-
def set_path_effects(self, path_effects):
453-
self._path_effects = path_effects
454-
455-
def get_path_effects(self):
456-
return self._path_effects
457-
458451
def set_bbox(self, rectprops):
459452
"""
460453
Draw a bounding box around self. rectprops are any settable

setupext.py

+1
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ def add_flags(self, ext):
744744
'agg_vcgen_contour.cpp',
745745
'agg_vcgen_dash.cpp',
746746
'agg_vcgen_stroke.cpp',
747+
'agg_vpgen_segmentator.cpp'
747748
]
748749
ext.sources.extend(
749750
os.path.join('agg24', 'src', x) for x in agg_sources)

src/path_converters.h

+2
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,8 @@ class Sketch
891891
inline void
892892
rewind(unsigned path_id)
893893
{
894+
unsigned short seed[] = {0, 0, 0};
895+
seed48(seed);
894896
m_has_last = false;
895897
m_p = 0.0;
896898
if (m_scale != 0.0) {

0 commit comments

Comments
 (0)