Skip to content

Commit 09d5f5f

Browse files
committed
Major refactor of path effects hook. Makes life much simpler!
1 parent 3d31865 commit 09d5f5f

23 files changed

+17182
-526
lines changed

doc/api/api_changes.rst

+11-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Changes in 1.4.x
1515
================
1616

1717
* A major refactoring of the axes module was made. The axes module has been
18-
splitted into smaller modules:
18+
split into smaller modules:
1919

2020
- the `_base` module, which contains a new private _AxesBase class. This
2121
class contains all methods except plotting and labelling methods.
@@ -30,8 +30,7 @@ original location:
3030
- math -> `import math`
3131
- ma -> `from numpy import ma`
3232
- cbook -> `from matplotlib import cbook`
33-
- division -> `from __future__ import division`
34-
- docstring -> `from matplotlib impotr docstring`
33+
- docstring -> `from matplotlib import docstring`
3534
- is_sequence_of_strings -> `from matplotlib.cbook import is_sequence_of_strings`
3635
- is_string_like -> `from matplotlib.cbook import is_string_like`
3736
- iterable -> `from matplotlib.cbook import iterable`
@@ -41,7 +40,7 @@ original location:
4140
- mcoll -> `from matplotlib import collections as mcoll`
4241
- mcolors -> `from matplotlib import colors as mcolors`
4342
- mcontour -> `from matplotlib import contour as mcontour`
44-
- mpatches -> `from matplotlib import patchs as mpatches`
43+
- mpatches -> `from matplotlib import patches as mpatches`
4544
- mpath -> `from matplotlib import path as mpath`
4645
- mquiver -> `from matplotlib import quiver as mquiver`
4746
- mstack -> `from matplotlib import stack as mstack`
@@ -55,8 +54,14 @@ original location:
5554
is 'open-high-low-close' order of quotes, which is the standard in finance.
5655

5756
* For consistency the ``face_alpha`` keyword to
58-
:class:`matplotlib.patheffects.SimplePatchShadow`'s has been deprecated.
59-
Use alpha instead.
57+
:class:`matplotlib.patheffects.SimplePatchShadow` has been deprecated in
58+
favour of the ``alpha`` keyword. Similarly, the keyword ``offset_xy`` is now
59+
named ``offset`` across all :class:`~matplotlib.patheffects.AbstractPathEffect`s.
60+
``matplotlib.patheffects._Base`` has
61+
been renamed to :class:`matplotlib.patheffects.AbstractPathEffect`.
62+
``matplotlib.patheffect.ProxyRenderer`` has been renamed to
63+
:class:`matplotlib.patheffects.PathEffectRenderer` and is now a full
64+
RendererBase subclass.
6065

6166
* The artist used to draw the outline of a `colorbar` has been changed
6267
from a `matplotlib.lines.Line2D` to `matplotlib.patches.Polygon`,

doc/api/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
mathtext_api.rst
3737
mlab_api.rst
3838
path_api.rst
39+
patheffects_api.rst
3940
pyplot_api.rst
4041
sankey_api.rst
4142
spines_api.rst

doc/api/patheffects_api.rst

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
***********
2+
patheffects
3+
***********
4+
5+
6+
:mod:`matplotlib.patheffects`
7+
=======================
8+
9+
.. automodule:: matplotlib.patheffects
10+
:members:
11+
:undoc-members:
12+
:show-inheritance:

doc/users/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ User's Guide
2828
transforms_tutorial.rst
2929
path_tutorial.rst
3030
annotations_guide.rst
31+
patheffects_guide.rst
3132
recipes.rst
3233
screenshots.rst
3334
whats_new.rst

doc/users/patheffects_guide.rst

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
.. _patheffects-guide:
2+
3+
******************
4+
Path effects guide
5+
******************
6+
7+
.. py:module:: matplotlib.patheffects
8+
9+
10+
Matplotlib's :mod:`~matplotlib.patheffects` module provides functionality to
11+
apply a multiple draw stage to any Artist which can be rendered via a
12+
:class:`~matplotlib.path.Path`.
13+
14+
Artists which can have a path effect applied to them include :class:`~matplotlib.patches.Patch`,
15+
:class:`~matplotlib.lines.Line2D`, :class:`~matplotlib.collections.Collection` and even
16+
:class:`~matplotlib.text.Text`. Each artist's path effects can be controlled via the
17+
``set_path_effects`` method (:class:`~matplotlib.artist.Artist.set_path_effects`), which takes
18+
an iterable of :class:`AbstractPathEffect` instances.
19+
20+
The simplest path effect is the :class:`Normal` effect, which simply
21+
draws the artist without any effect:
22+
23+
.. plot::
24+
:include-source:
25+
26+
import matplotlib.pyplot as plt
27+
import matplotlib.patheffects as path_effects
28+
29+
fig = plt.figure(figsize=(5, 1.5))
30+
text = fig.text(0.5, 0.5, 'Hello path effects world!\nThis is the normal '
31+
'path effect.\nPretty dull, huh?',
32+
ha='center', va='center', size=20)
33+
text.set_path_effects([path_effects.Normal()])
34+
plt.show()
35+
36+
Whilst the plot doesn't look any different to what you would expect without any path
37+
effects, the drawing of the text now been changed to use the the path effects
38+
framework, opening up the possibilities for more interesting examples.
39+
40+
Adding a shadow
41+
---------------
42+
43+
A far more interesting path effect than :class:`Normal` is the
44+
drop-shadow, which we can apply to any of our path based artists. The classes
45+
:class:`SimplePatchShadow` and
46+
:class:`SimpleLineShadow` do precisely this by drawing either a filled
47+
patch or a line patch below the original artist:
48+
49+
.. plot::
50+
:include-source:
51+
52+
import matplotlib.pyplot as plt
53+
import matplotlib.patheffects as path_effects
54+
55+
text = plt.text(0.5, 0.5, 'Hello path effects world!',
56+
path_effects=[path_effects.withSimplePatchShadow()])
57+
58+
plt.plot([0, 3, 2, 5], linewidth=5, color='blue',
59+
path_effects=[path_effects.SimpleLineShadow(),
60+
path_effects.Normal()])
61+
plt.show()
62+
63+
64+
Notice the two approaches to setting the path effects in this example. The
65+
first uses the ``with*`` classes to include the desired functionality automatically
66+
followed with the "normal" effect, whereas the latter explicitly defines the two path
67+
effects to draw.
68+
69+
Making an artist stand out
70+
--------------------------
71+
72+
One nice way of making artists visually stand out is to draw an outline in a bold
73+
color below the actual artist. The :class:`Stroke` path effect
74+
makes this a relatively simple task:
75+
76+
.. plot::
77+
:include-source:
78+
79+
import matplotlib.pyplot as plt
80+
import matplotlib.patheffects as path_effects
81+
82+
fig = plt.figure(figsize=(7, 1))
83+
text = fig.text(0.5, 0.5, 'This text stands out because of\n'
84+
'its black border.', color='white',
85+
ha='center', va='center', size=30)
86+
text.set_path_effects([path_effects.Stroke(linewidth=3, foreground='black'),
87+
path_effects.Normal()])
88+
plt.show()
89+
90+
It is important to note that this effect only works because we have drawn the text
91+
path twice; once with a thick black line, and then once with the original text
92+
path on top.
93+
94+
You may have noticed that the keywords to :class:`Stroke` and
95+
:class:`SimplePatchShadow` and :class:`SimpleLineShadow` are not the usual Artist
96+
keywords (such as ``facecolor`` and ``edgecolor`` etc.). This is because with these
97+
path effects we are operating at lower level of matplotlib. In fact, the keywords
98+
which are accepted are those for a :class:`matplotlib.backend_bases.GraphicsContextBase`
99+
instance, which have been designed for making it easy to create new backends - and not
100+
for its user interface.
101+
102+
103+
Greater control of the path effect artist
104+
-----------------------------------------
105+
106+
As already mentioned, some of the path effects operate at a lower level than most users
107+
will be used to, meaning that setting keywords such as ``facecolor`` and ``edgecolor``
108+
raise an AttributeError. Luckily there is a generic :class:`PathPatchEffect` path effect
109+
which creates a :class:`~matplotlib.patches.PathPatch` class with the original path.
110+
The keywords to this effect are identical to those of :class:`~matplotlib.patches.PathPatch`:
111+
112+
.. plot::
113+
:include-source:
114+
115+
import matplotlib.pyplot as plt
116+
import matplotlib.patheffects as path_effects
117+
118+
fig = plt.figure(figsize=(8, 1))
119+
t = fig.text(0.02, 0.5, 'Hatch shadow', fontsize=75, weight=1000, va='center')
120+
t.set_path_effects([path_effects.PathPatchEffect(offset=(4, -4), hatch='xxxx',
121+
facecolor='gray'),
122+
path_effects.PathPatchEffect(edgecolor='white', linewidth=1.1,
123+
facecolor='black')])
124+
plt.show()
125+
126+
127+
..
128+
Headings for future consideration:
129+
130+
Implementing a custom path effect
131+
---------------------------------
132+
133+
What is going on under the hood
134+
--------------------------------

doc/users/whats_new.rst

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ revision, see the :ref:`github-stats`.
2323
new in matplotlib-1.4
2424
=====================
2525

26+
Documentation changes
27+
---------------------
28+
29+
Phil Elson rewrote of the documentation and userguide for both Legend and PathEffects (links needed).
30+
31+
2632
New plotting features
2733
---------------------
2834

@@ -74,6 +80,7 @@ and :func:`matplotlib.dates.datestr2num`. Support is also added to the unit
7480
conversion interfaces :class:`matplotlib.dates.DateConverter` and
7581
:class:`matplotlib.units.Registry`.
7682

83+
7784
Configuration (rcParams)
7885
------------------------
7986

lib/matplotlib/backend_bases.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -125,22 +125,25 @@ def mainloop(self):
125125
pass
126126

127127

128-
class RendererBase:
128+
class RendererBase(object):
129129
"""An abstract base class to handle drawing/rendering operations.
130130
131-
The following methods *must* be implemented in the backend:
131+
The following methods must be implemented in the backend for full
132+
functionality (though just implementing :meth:`draw_path` alone would
133+
give a highly capable backend):
132134
133135
* :meth:`draw_path`
134136
* :meth:`draw_image`
135-
* :meth:`draw_text`
136-
* :meth:`get_text_width_height_descent`
137+
* :meth:`draw_gouraud_triangle`
137138
138139
The following methods *should* be implemented in the backend for
139140
optimization reasons:
140141
142+
* :meth:`draw_text`
141143
* :meth:`draw_markers`
142144
* :meth:`draw_path_collection`
143145
* :meth:`draw_quad_mesh`
146+
144147
"""
145148
def __init__(self):
146149
self._texmanager = None
@@ -392,7 +395,7 @@ def _iter_collection(self, gc, master_transform, all_transforms,
392395
if not (np.isfinite(xo) and np.isfinite(yo)):
393396
continue
394397
if Nfacecolors:
395-
rgbFace = tuple(facecolors[i % Nfacecolors])
398+
rgbFace = facecolors[i % Nfacecolors]
396399
if Nedgecolors:
397400
if Nlinewidths:
398401
gc0.set_linewidth(linewidths[i % Nlinewidths])
@@ -558,7 +561,6 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):
558561
*ismath*
559562
If True, use mathtext parser. If "TeX", use *usetex* mode.
560563
"""
561-
562564
path, transform = self._get_text_path_transform(
563565
x, y, s, prop, angle, ismath)
564566
color = gc.get_rgb()

lib/matplotlib/backends/backend_agg.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def draw_path(self, gc, path, transform, rgbFace=None):
130130
nmax = rcParams['agg.path.chunksize'] # here at least for testing
131131
npts = path.vertices.shape[0]
132132
if (nmax > 100 and npts > nmax and path.should_simplify and
133-
rgbFace is None and gc.get_hatch() is None):
133+
rgbFace is None and gc.get_hatch() is None):
134134
nch = np.ceil(npts/float(nmax))
135135
chsize = int(np.ceil(npts/nch))
136136
i0 = np.arange(0, npts, chsize)
@@ -196,7 +196,7 @@ def get_text_width_height_descent(self, s, prop, ismath):
196196
get the width and height in display coords of the string s
197197
with FontPropertry prop
198198
199-
# passing rgb is a little hack to make cacheing in the
199+
# passing rgb is a little hack to make caching in the
200200
# texmanager more efficient. It is not meant to be used
201201
# outside the backend
202202
"""

lib/matplotlib/backends/backend_mixed.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def __init__(self, figure, width, height, dpi, vector_renderer,
4949
self._raster_renderer = None
5050
self._rasterizing = 0
5151

52-
# A renference to the figure is needed as we need to change
52+
# A reference to the figure is needed as we need to change
5353
# the figure dpi before and after the rasterization. Although
5454
# this looks ugly, I couldn't find a better solution. -JJL
5555
self.figure=figure
@@ -67,6 +67,7 @@ def __init__(self, figure, width, height, dpi, vector_renderer,
6767
option_image_nocomposite points_to_pixels strip_math
6868
start_filter stop_filter draw_gouraud_triangle
6969
draw_gouraud_triangles option_scale_image
70+
_text2path _get_text_path_transform height width
7071
""".split()
7172
def _set_current_renderer(self, renderer):
7273
self._renderer = renderer
@@ -77,7 +78,6 @@ def _set_current_renderer(self, renderer):
7778
renderer.start_rasterizing = self.start_rasterizing
7879
renderer.stop_rasterizing = self.stop_rasterizing
7980

80-
8181
def start_rasterizing(self):
8282
"""
8383
Enter "raster" mode. All subsequent drawing commands (until

lib/matplotlib/backends/backend_pdf.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -1449,10 +1449,10 @@ def finalize(self):
14491449
self.file.output(*self.gc.finalize())
14501450

14511451
def check_gc(self, gc, fillcolor=None):
1452-
orig_fill = gc._fillcolor
1452+
orig_fill = getattr(gc, '_fillcolor', (0., 0., 0.))
14531453
gc._fillcolor = fillcolor
14541454

1455-
orig_alphas = gc._effective_alphas
1455+
orig_alphas = getattr(gc, '_effective_alphas', (1.0, 1.0))
14561456

14571457
if gc._forced_alpha:
14581458
gc._effective_alphas = (gc._alpha, gc._alpha)
@@ -2207,8 +2207,11 @@ def copy_properties(self, other):
22072207
Copy properties of other into self.
22082208
"""
22092209
GraphicsContextBase.copy_properties(self, other)
2210-
self._fillcolor = other._fillcolor
2211-
self._effective_alphas = other._effective_alphas
2210+
fillcolor = getattr(other, '_fillcolor', self._fillcolor)
2211+
effective_alphas = getattr(other, '_effective_alphas',
2212+
self._effective_alphas)
2213+
self._fillcolor = fillcolor
2214+
self._effective_alphas = effective_alphas
22122215

22132216
def finalize(self):
22142217
"""

0 commit comments

Comments
 (0)