Skip to content

Commit e7353a1

Browse files
committed
REORG: JoinStyle and CapStyle classes
Centralize docs and validation for JoinStyle and CapStyle in one place.
1 parent c5ab728 commit e7353a1

18 files changed

+354
-167
lines changed

doc/api/_types.rst

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

doc/api/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ Matplotlib consists of the following submodules:
123123
transformations.rst
124124
tri_api.rst
125125
type1font.rst
126+
_types.rst
126127
units_api.rst
127128
widgets_api.rst
128129

lib/matplotlib/_types.py

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""
2+
Style desription information that is shared across unrelated classses.
3+
"""
4+
5+
from enum import Enum
6+
from matplotlib import cbook
7+
8+
9+
def _deprecate_case_insensitive_join_cap(s):
10+
s_low = s.lower()
11+
if s != s_low:
12+
if s_low in ['miter', 'round', 'bevel']:
13+
cbook.warn_deprecated(
14+
"3.3", message="Case-insensitive capstyles are deprecated "
15+
"since %(since)s and support for them will be removed "
16+
"%(removal)s; please pass them in lowercase.")
17+
elif s_low in ['butt', 'round', 'projecting']:
18+
cbook.warn_deprecated(
19+
"3.3", message="Case-insensitive joinstyles are deprecated "
20+
"since %(since)s and support for them will be removed "
21+
"%(removal)s; please pass them in lowercase.")
22+
# Else, error out at the check_in_list stage.
23+
return s_low
24+
25+
26+
class JoinStyle(Enum):
27+
"""
28+
Define how the connection between two line segments is drawn.
29+
30+
For a simple visual description of each *JoinStyle*, `view these docs
31+
online <JoinStyle>`, or simply run `JoinStyle.demo`.
32+
33+
.. plot::
34+
:alt: Demo of possible JoinStyle's
35+
36+
from matplotlib._types import JoinStyle
37+
JoinStyle.demo()
38+
39+
For a more precise description, we first recall that lines in Matplotlib
40+
are typically defined by a 1D `~.path.Path` and a finite ``linewidth``,
41+
where the underlying 1D `~.path.Path` represents the center of the stroked
42+
line.
43+
44+
By default, `~.backend_bases.GraphicsContextBase` defines the boundaries of
45+
a stroked line to simply be every point within some radius,
46+
``linewidth/2``, away from any point of the center line. However, this
47+
results in corners appearing "rounded", which may not be the desired
48+
behavior if you are drawing, for example, a polygon or pointed star.
49+
50+
Matplotlib provides three options for defining how the corners where two
51+
segments meet should be drawn. In short:
52+
53+
- *miter* is the "arrow-tip" style. Each boundary of the filled-in area
54+
will extend in a straight line parallel to the tangent vector of the
55+
centerline at the point it meets the corner, until they meet in a
56+
sharp point.
57+
- *round* stokes every point within a radius of ``linewidth/2`` of the
58+
center lines.
59+
- *bevel* is the "squared-off" style. It can be thought of as a rounded
60+
corner where the "circular" part of the corner has been cut off.
61+
62+
.. note::
63+
64+
The *miter* option can be controller further by specifying a "miter
65+
limit", which specifies how long a miter tip can get before it is
66+
automatically "bevel"ed off. Matplotlib does not currently expose a
67+
``miterlimit`` parameter to the user, and most backends simply use the
68+
upstream default value. For example, the PDF backend assumes the
69+
default value of 10 specified by the PDF standard, while the SVG
70+
backend does not even specify the miter limit, resulting in a default
71+
value of 4 per the SVG specification.
72+
73+
A more detailed description of the effect of a miter limit can be found
74+
in the `Mozilla Developer Docs
75+
<https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit>`_
76+
"""
77+
78+
miter = 'miter'
79+
round = 'round'
80+
bevel = 'bevel'
81+
82+
def __init__(self, s):
83+
s = _deprecate_case_insensitive_join_cap(s)
84+
Enum.__init__(self)
85+
86+
@staticmethod
87+
def demo():
88+
import numpy as np
89+
import matplotlib.pyplot as plt
90+
91+
def plot_angle(ax, x, y, angle, style):
92+
phi = np.radians(angle)
93+
xx = [x + .5, x, x + .5*np.cos(phi)]
94+
yy = [y, y, y + .5*np.sin(phi)]
95+
ax.plot(xx, yy, lw=12, color='tab:blue', solid_joinstyle=style)
96+
ax.plot(xx, yy, lw=1, color='black')
97+
ax.plot(xx[1], yy[1], 'o', color='tab:red', markersize=3)
98+
99+
fig, ax = plt.subplots(figsize=(8, 6))
100+
ax.set_title('Join style')
101+
for x, style in enumerate(['miter', 'round', 'bevel']):
102+
ax.text(x, 5, style)
103+
for y, angle in enumerate([20, 45, 60, 90, 120]):
104+
plot_angle(ax, x, y, angle, style)
105+
if x == 0:
106+
ax.text(-1.3, y, f'{angle} degrees')
107+
ax.set_xlim(-1.5, 2.75)
108+
ax.set_ylim(-.5, 5.5)
109+
ax.set_axis_off()
110+
111+
plt.show()
112+
113+
114+
class CapStyle(Enum):
115+
"""
116+
Define how the the end of a line is drawn.
117+
118+
How to draw the start and end points of lines that represent a closed curve
119+
(i.e. that end in a `~.path.Path.CLOSEPOLY`) is controlled by the line's
120+
`JoinStyle`. For all other lines, how the start and end points are drawn is
121+
controlled by the *CapStyle*.
122+
123+
For a simple visual description of each *CapStyle*, `view these docs
124+
online <CapStyle>` or simply run `CapStyle.demo`.
125+
126+
.. plot::
127+
:alt: Demo of possible CapStyle's
128+
129+
from matplotlib._types import CapStyle
130+
CapStyle.demo()
131+
132+
Briefly, the three options available are:
133+
134+
- *butt*: the line is squared off at its endpoint.
135+
- *projecting*: the line is squared off as in *butt*, but the filled in
136+
area extends beyond the endpoint a distance of ``linewidth/2``.
137+
- *round*: like *butt*, but a semicircular cap is added to the end of
138+
the line, of radius ``linewidth/2``.
139+
"""
140+
butt = 'butt'
141+
projecting = 'projecting'
142+
round = 'round'
143+
144+
def __init__(self, s):
145+
s = _deprecate_case_insensitive_join_cap(s)
146+
Enum.__init__(self)
147+
148+
@staticmethod
149+
def demo():
150+
import matplotlib.pyplot as plt
151+
152+
fig, ax = plt.subplots(figsize=(8, 2))
153+
ax.set_title('Cap style')
154+
155+
for x, style in enumerate(['butt', 'round', 'projecting']):
156+
ax.text(x+0.25, 1, style, ha='center')
157+
xx = [x, x+0.5]
158+
yy = [0, 0]
159+
ax.plot(xx, yy, lw=12, color='tab:blue', solid_capstyle=style)
160+
ax.plot(xx, yy, lw=1, color='black')
161+
ax.plot(xx, yy, 'o', color='tab:red', markersize=3)
162+
ax.text(2.25, 0.7, '(default)', ha='center')
163+
164+
ax.set_ylim(-.5, 1.5)
165+
ax.set_axis_off()

lib/matplotlib/backend_bases.py

+22-11
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
from matplotlib.backend_managers import ToolManager
5050
from matplotlib.cbook import _setattr_cm
5151
from matplotlib.path import Path
52-
from matplotlib.rcsetup import validate_joinstyle, validate_capstyle
5352
from matplotlib.transforms import Affine2D
53+
from matplotlib._types import JoinStyle, CapStyle
5454

5555

5656
_log = logging.getLogger(__name__)
@@ -764,11 +764,11 @@ def __init__(self):
764764
self._alpha = 1.0
765765
self._forced_alpha = False # if True, _alpha overrides A from RGBA
766766
self._antialiased = 1 # use 0, 1 not True, False for extension code
767-
self._capstyle = 'butt'
767+
self._capstyle = CapStyle('butt')
768768
self._cliprect = None
769769
self._clippath = None
770770
self._dashes = 0, None
771-
self._joinstyle = 'round'
771+
self._joinstyle = JoinStyle('round')
772772
self._linestyle = 'solid'
773773
self._linewidth = 1
774774
self._rgb = (0.0, 0.0, 0.0, 1.0)
@@ -820,7 +820,7 @@ def get_antialiased(self):
820820

821821
def get_capstyle(self):
822822
"""
823-
Return the capstyle as a string in ('butt', 'round', 'projecting').
823+
Return the default `.CapStyle`.
824824
"""
825825
return self._capstyle
826826

@@ -866,7 +866,7 @@ def get_forced_alpha(self):
866866
return self._forced_alpha
867867

868868
def get_joinstyle(self):
869-
"""Return the line join style as one of ('miter', 'round', 'bevel')."""
869+
"""Return the `.JoinStyle`."""
870870
return self._joinstyle
871871

872872
def get_linewidth(self):
@@ -919,9 +919,15 @@ def set_antialiased(self, b):
919919
self._antialiased = int(bool(b))
920920

921921
def set_capstyle(self, cs):
922-
"""Set the capstyle to be one of ('butt', 'round', 'projecting')."""
923-
validate_capstyle(cs)
924-
self._capstyle = cs
922+
"""
923+
Set the `.CapStyle`.
924+
925+
Parameters
926+
----------
927+
cs : `.CapStyle` or {'butt', 'round', 'projecting'}
928+
How to draw end points of lines.
929+
"""
930+
self._capstyle = CapStyle(cs)
925931

926932
def set_clip_rectangle(self, rectangle):
927933
"""
@@ -987,9 +993,14 @@ def set_foreground(self, fg, isRGBA=False):
987993
self._rgb = colors.to_rgba(fg)
988994

989995
def set_joinstyle(self, js):
990-
"""Set the join style to be one of ('miter', 'round', 'bevel')."""
991-
validate_joinstyle(js)
992-
self._joinstyle = js
996+
"""
997+
Set the `.JoinStyle`.
998+
999+
Parameters
1000+
----------
1001+
js : `.JoinStyle` or {'miter', 'round', 'bevel'}.
1002+
"""
1003+
self._joinstyle = JoinStyle(js)
9931004

9941005
def set_linewidth(self, w):
9951006
"""Set the linewidth in points."""

lib/matplotlib/backends/backend_cairo.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from matplotlib.mathtext import MathTextParser
3333
from matplotlib.path import Path
3434
from matplotlib.transforms import Affine2D
35+
from matplotlib._types import JoinStyle, CapStyle
3536

3637

3738
backend_version = cairo.version
@@ -326,15 +327,15 @@ def points_to_pixels(self, points):
326327

327328
class GraphicsContextCairo(GraphicsContextBase):
328329
_joind = {
329-
'bevel': cairo.LINE_JOIN_BEVEL,
330-
'miter': cairo.LINE_JOIN_MITER,
331-
'round': cairo.LINE_JOIN_ROUND,
330+
JoinStyle.bevel: cairo.LINE_JOIN_BEVEL,
331+
JoinStyle.miter: cairo.LINE_JOIN_MITER,
332+
JoinStyle.round: cairo.LINE_JOIN_ROUND,
332333
}
333334

334335
_capd = {
335-
'butt': cairo.LINE_CAP_BUTT,
336-
'projecting': cairo.LINE_CAP_SQUARE,
337-
'round': cairo.LINE_CAP_ROUND,
336+
CapStyle.butt: cairo.LINE_CAP_BUTT,
337+
CapStyle.projecting: cairo.LINE_CAP_SQUARE,
338+
CapStyle.round: cairo.LINE_CAP_ROUND,
338339
}
339340

340341
def __init__(self, renderer):
@@ -358,8 +359,8 @@ def set_alpha(self, alpha):
358359
# one for False.
359360

360361
def set_capstyle(self, cs):
361-
self.ctx.set_line_cap(_api.check_getitem(self._capd, capstyle=cs))
362-
self._capstyle = cs
362+
super().set_capstyle(cs)
363+
self.ctx.set_line_cap(self._capd[self.get_capstyle()])
363364

364365
def set_clip_rectangle(self, rectangle):
365366
if not rectangle:
@@ -401,8 +402,8 @@ def get_rgb(self):
401402
return self.ctx.get_source().get_rgba()[:3]
402403

403404
def set_joinstyle(self, js):
404-
self.ctx.set_line_join(_api.check_getitem(self._joind, joinstyle=js))
405-
self._joinstyle = js
405+
super().set_joinstyle(js)
406+
self.ctx.set_line_join(self._joind[self.get_joinstyle()])
406407

407408
def set_linewidth(self, w):
408409
self._linewidth = float(w)

lib/matplotlib/backends/backend_pdf.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,23 @@
2626
import matplotlib as mpl
2727
from matplotlib import _text_layout, cbook
2828
from matplotlib._pylab_helpers import Gcf
29+
from matplotlib.afm import AFM
2930
from matplotlib.backend_bases import (
3031
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
3132
GraphicsContextBase, RendererBase)
3233
from matplotlib.backends.backend_mixed import MixedModeRenderer
34+
from matplotlib.dates import UTC
35+
import matplotlib.dviread as dviread
3336
from matplotlib.figure import Figure
3437
from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font
35-
from matplotlib.afm import AFM
36-
import matplotlib.type1font as type1font
37-
import matplotlib.dviread as dviread
3838
from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE,
3939
LOAD_NO_HINTING, KERNING_UNFITTED)
4040
from matplotlib.mathtext import MathTextParser
41-
from matplotlib.transforms import Affine2D, BboxBase
42-
from matplotlib.path import Path
43-
from matplotlib.dates import UTC
4441
from matplotlib import _path
42+
from matplotlib.path import Path
43+
from matplotlib._types import JoinStyle, CapStyle
44+
import matplotlib.type1font as type1font
45+
from matplotlib.transforms import Affine2D, BboxBase
4546
from . import _backend_pdf_ps
4647

4748
_log = logging.getLogger(__name__)
@@ -738,7 +739,8 @@ def newPage(self, width, height):
738739
self.reserveObject('length of content stream'))
739740
# Initialize the pdf graphics state to match the default mpl
740741
# graphics context: currently only the join style needs to be set
741-
self.output(GraphicsContextPdf.joinstyles['round'], Op.setlinejoin)
742+
self.output(GraphicsContextPdf.joinstyles[JoinStyle.round],
743+
Op.setlinejoin)
742744

743745
# Clear the list of annotations for the next page
744746
self.pageAnnotations = []
@@ -2377,8 +2379,8 @@ def paint(self):
23772379
"""
23782380
return Op.paint_path(self.fill(), self.stroke())
23792381

2380-
capstyles = {'butt': 0, 'round': 1, 'projecting': 2}
2381-
joinstyles = {'miter': 0, 'round': 1, 'bevel': 2}
2382+
capstyles = {CapStyle.butt: 0, CapStyle.round: 1, CapStyle.projecting: 2}
2383+
joinstyles = {JoinStyle.miter: 0, JoinStyle.round: 1, JoinStyle.bevel: 2}
23822384

23832385
def capstyle_cmd(self, style):
23842386
return [self.capstyles[style], Op.setlinecap]

lib/matplotlib/backends/backend_pgf.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from matplotlib.path import Path
2727
from matplotlib.figure import Figure
2828
from matplotlib._pylab_helpers import Gcf
29+
from matplotlib._types import JoinStyle, CapStyle
2930

3031
_log = logging.getLogger(__name__)
3132

@@ -531,15 +532,15 @@ def _print_pgf_clip(self, gc):
531532

532533
def _print_pgf_path_styles(self, gc, rgbFace):
533534
# cap style
534-
capstyles = {"butt": r"\pgfsetbuttcap",
535-
"round": r"\pgfsetroundcap",
536-
"projecting": r"\pgfsetrectcap"}
535+
capstyles = {CapStyle.butt: r"\pgfsetbuttcap",
536+
CapStyle.round: r"\pgfsetroundcap",
537+
CapStyle.projecting: r"\pgfsetrectcap"}
537538
writeln(self.fh, capstyles[gc.get_capstyle()])
538539

539540
# join style
540-
joinstyles = {"miter": r"\pgfsetmiterjoin",
541-
"round": r"\pgfsetroundjoin",
542-
"bevel": r"\pgfsetbeveljoin"}
541+
joinstyles = {JoinStyle.miter: r"\pgfsetmiterjoin",
542+
JoinStyle.round: r"\pgfsetroundjoin",
543+
JoinStyle.bevel: r"\pgfsetbeveljoin"}
543544
writeln(self.fh, joinstyles[gc.get_joinstyle()])
544545

545546
# filling

0 commit comments

Comments
 (0)