Skip to content

REORG: JoinStyle and CapStyle classes #18544

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

Merged
merged 4 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions doc/_static/mpl.css
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ table.highlighttable td {
padding: 0 0.5em 0 0.5em;
}

cite, code, tt {
cite, code, tt, dl.value-list dt {
font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
font-size: 0.95em;
letter-spacing: 0.01em;
Expand Down Expand Up @@ -730,7 +730,6 @@ td.field-body table.property-table tr:last-of-type td {
border-bottom-color: #888;
}


/* function and class description */
.descclassname {
color: #aaa;
Expand Down Expand Up @@ -806,6 +805,22 @@ dl.class > dd {
font-size: 14px;
}

/* custom tables for lists of allowed values in "mpl._types" */
dl.value-list {
display: grid;
}

dl.value-list dt {
grid-column: 1;
margin: 4px 0;
}

dl.value-list dd {
grid-column: 2;
margin: 4px 0 4px 20px;
padding: 0;
}

/* parameter section table */
table.docutils.field-list {
width: 100%;
Expand Down Expand Up @@ -1257,4 +1272,4 @@ div.bullet-box li {

div#gallery.section .sphx-glr-clear:first-of-type, div#tutorials.section .sphx-glr-clear:first-of-type{
display: none;
}
}
15 changes: 15 additions & 0 deletions doc/api/_enums.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
**********************
``matplotlib._enums``
**********************

.. automodule:: matplotlib._enums
:no-members:

.. autoclass:: JoinStyle
:members: demo
:exclude-members: bevel, miter, round, input_description

.. autoclass:: CapStyle
:members: demo
:exclude-members: butt, round, projecting, input_description

1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Matplotlib consists of the following submodules:
dates_api.rst
docstring_api.rst
dviread.rst
_enums.rst
figure_api.rst
font_manager_api.rst
fontconfig_pattern_api.rst
Expand Down
15 changes: 15 additions & 0 deletions examples/lines_bars_and_markers/capstyle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
=========
CapStyle
=========

The `matplotlib._enums.CapStyle` controls how Matplotlib draws the corners
where two different line segments meet. For more details, see the
`~matplotlib._enums.CapStyle` docs.
"""

import matplotlib.pyplot as plt
from matplotlib._enums import CapStyle

CapStyle.demo()
plt.show()
91 changes: 8 additions & 83 deletions examples/lines_bars_and_markers/joinstyle.py
Original file line number Diff line number Diff line change
@@ -1,90 +1,15 @@
"""
==========================
Copy link
Member

Choose a reason for hiding this comment

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

Why was this example deleted? If you moved the docs to a class defn, fine, but leave the stub here with a link to the docs? Maybe w/ a small minimal example?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't feel like it made sense to have the exact same example in two different places. My goal with these API docs was to be a strictly-better drop-in replacement for this example.

Is the concern here that the code that makes (what is now) the JoinStyle.demo figure is not as easily accessible in the docs anymore? I would propose that the example was not really adding much to the documentation via its code, but only really via its output (the visual representation of each Join/CapStyle is what's important).

Is the concern discoverability? I know that we have issues with Google sending people to old versions of the docs, but presumably the hope is that the new page quickly gets indexed as the go-to place for JoinStyle. In which case the relevant example figure will still be there.

If the concern is that people looking for that specific example will be confused as to where it went, then my impression of what's the best way to leave a "stub" behind is, instead of leaving an actual stub page with a link, we should really just use something like the reredirects package to simply have an HTTP 301 response whenever someone tries to visit that page. As a side-effect this will also help with Google search indexing.

Copy link
Member

Choose a reason for hiding this comment

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

Something like the redirects package looks very interesting, though version 0.0.0 is a negative.

But the other concern is that the example will no longer will show up in the example gallery...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haha fair, didn't realize the version number was so sketch, but frankly I can white room that package in a couple hours if we wanted to have our own implementation.

Gitlab reports that the whole "package" is only 62 lines of Python code. Was mostly linking it as a proof-of-principle that this is not impossible to do inside of Sphinx.

This may be totally off-base, but I struggle to imagine the user that scrolls through the examples gallery and is enticed to click on the JoinStyle demo in particular. Unless you know what it is already? But people that already know the name for something typically find examples by getting directly linked in via Google...

If I'm outvoted on that point, I guess we would just add two stub examples, one which just calls CapStyle.demo and one that calls JoinStyle.demo?

Copy link
Member

Choose a reason for hiding this comment

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

If you are browsing the examples, and come across it you learn something new. If you don't know what a join style is, you can never google it. People need some way to know about the features in the library, and the example pages are basically it.

If you just want to have it call and link the class demo, thats perhaps fine. Then the plot shows up, and the user is linked to complete docs. But I think leaving it in as an example in the gallery is useful.

Copy link
Member

Choose a reason for hiding this comment

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

I think adding redirects is not the issue, but collating through the hundreds of old versions is a chore.

As for examples, I thought there was some talk of putting a smaller set of examples for all plot types, and maybe we could do the same for all these new style classes?

Copy link
Member

Choose a reason for hiding this comment

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

^ that would be great. However I don't think we should delete existing examples until that is created?

Copy link
Member

Choose a reason for hiding this comment

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

👍 for leaving stub examples and linking to the class. and later collecting these examples in a dedicated section.

(This is for discoverability rather than for keeping links alive).

Join styles and cap styles
==========================
=========
JoinStyle
=========

This example demonstrates the available join styles and cap styles.

Both are used in `.Line2D` and various ``Collections`` from
`matplotlib.collections` as well as some functions that create these, e.g.
`~matplotlib.pyplot.plot`.


Join styles
===========

Join styles define how the connection between two line segments is drawn.

See the respective ``solid_joinstyle``, ``dash_joinstyle`` or ``joinstyle``
parameters.
The `matplotlib._enums.JoinStyle` controls how Matplotlib draws the corners
where two different line segments meet. For more details, see the
`~matplotlib._enums.JoinStyle` docs.
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib._enums import JoinStyle


def plot_angle(ax, x, y, angle, style):
phi = np.radians(angle)
xx = [x + .5, x, x + .5*np.cos(phi)]
yy = [y, y, y + .5*np.sin(phi)]
ax.plot(xx, yy, lw=12, color='tab:blue', solid_joinstyle=style)
ax.plot(xx, yy, lw=1, color='black')
ax.plot(xx[1], yy[1], 'o', color='tab:red', markersize=3)


fig, ax = plt.subplots(figsize=(8, 6))
ax.set_title('Join style')

for x, style in enumerate(['miter', 'round', 'bevel']):
ax.text(x, 5, style)
for y, angle in enumerate([20, 45, 60, 90, 120]):
plot_angle(ax, x, y, angle, style)
if x == 0:
ax.text(-1.3, y, f'{angle} degrees')
ax.text(1, 4.7, '(default)')

ax.set_xlim(-1.5, 2.75)
ax.set_ylim(-.5, 5.5)
ax.set_axis_off()
JoinStyle.demo()
plt.show()


#############################################################################
#
# Cap styles
# ==========
#
# Cap styles define how the the end of a line is drawn.
#
# See the respective ``solid_capstyle``, ``dash_capstyle`` or ``capstyle``
# parameters.

fig, ax = plt.subplots(figsize=(8, 2))
ax.set_title('Cap style')

for x, style in enumerate(['butt', 'round', 'projecting']):
ax.text(x+0.25, 1, style, ha='center')
xx = [x, x+0.5]
yy = [0, 0]
ax.plot(xx, yy, lw=12, color='tab:blue', solid_capstyle=style)
ax.plot(xx, yy, lw=1, color='black')
ax.plot(xx, yy, 'o', color='tab:red', markersize=3)
ax.text(2.25, 0.7, '(default)', ha='center')

ax.set_ylim(-.5, 1.5)
ax.set_axis_off()


#############################################################################
#
# ------------
#
# References
# """"""""""
#
# The use of the following functions, methods, classes and modules is shown
# in this example:

import matplotlib
matplotlib.axes.Axes.plot
matplotlib.pyplot.plot
208 changes: 208 additions & 0 deletions lib/matplotlib/_enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"""
Enums representing sets of strings that Matplotlib uses as input parameters.

Matplotlib often uses simple data types like strings or tuples to define a
concept; e.g. the line capstyle can be specified as one of 'butt', 'round',
or 'projecting'. The classes in this module are used internally and serve to
document these concepts formally.

As an end-user you will not use these classes directly, but only the values
they define.
"""

from enum import Enum, auto
from matplotlib import cbook, docstring


class _AutoStringNameEnum(Enum):
"""Automate the ``name = 'name'`` part of making a (str, Enum)."""

def _generate_next_value_(name, start, count, last_values):
return name

def __hash__(self):
return str(self).__hash__()


def _deprecate_case_insensitive_join_cap(s):
s_low = s.lower()
if s != s_low:
if s_low in ['miter', 'round', 'bevel']:
cbook.warn_deprecated(
"3.3", message="Case-insensitive capstyles are deprecated "
"since %(since)s and support for them will be removed "
"%(removal)s; please pass them in lowercase.")
elif s_low in ['butt', 'round', 'projecting']:
cbook.warn_deprecated(
"3.3", message="Case-insensitive joinstyles are deprecated "
"since %(since)s and support for them will be removed "
"%(removal)s; please pass them in lowercase.")
# Else, error out at the check_in_list stage.
return s_low


class JoinStyle(str, _AutoStringNameEnum):
"""
Define how the connection between two line segments is drawn.

For a visual impression of each *JoinStyle*, `view these docs online
<JoinStyle>`, or run `JoinStyle.demo`.

Lines in Matplotlib are typically defined by a 1D `~.path.Path` and a
finite ``linewidth``, where the underlying 1D `~.path.Path` represents the
center of the stroked line.

By default, `~.backend_bases.GraphicsContextBase` defines the boundaries of
a stroked line to simply be every point within some radius,
``linewidth/2``, away from any point of the center line. However, this
results in corners appearing "rounded", which may not be the desired
behavior if you are drawing, for example, a polygon or pointed star.

**Supported values:**

.. rst-class:: value-list

'miter'
the "arrow-tip" style. Each boundary of the filled-in area will
extend in a straight line parallel to the tangent vector of the
centerline at the point it meets the corner, until they meet in a
sharp point.
'round'
stokes every point within a radius of ``linewidth/2`` of the center
lines.
'bevel'
the "squared-off" style. It can be thought of as a rounded corner
where the "circular" part of the corner has been cut off.

.. note::

Very long miter tips are cut off (to form a *bevel*) after a
backend-dependent limit called the "miter limit", which specifies the
maximum allowed ratio of miter length to line width. For example, the
PDF backend uses the default value of 10 specified by the PDF standard,
while the SVG backend does not even specify the miter limit, resulting
in a default value of 4 per the SVG specification. Matplotlib does not
currently allow the user to adjust this parameter.

A more detailed description of the effect of a miter limit can be found
in the `Mozilla Developer Docs
<https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit>`_

.. plot::
:alt: Demo of possible JoinStyle's

from matplotlib._enums import JoinStyle
JoinStyle.demo()

"""

miter = auto()
round = auto()
bevel = auto()

def __init__(self, s):
s = _deprecate_case_insensitive_join_cap(s)
Enum.__init__(self)

@staticmethod
def demo():
"""Demonstrate how each JoinStyle looks for various join angles."""
import numpy as np
import matplotlib.pyplot as plt

def plot_angle(ax, x, y, angle, style):
phi = np.radians(angle)
xx = [x + .5, x, x + .5*np.cos(phi)]
yy = [y, y, y + .5*np.sin(phi)]
ax.plot(xx, yy, lw=12, color='tab:blue', solid_joinstyle=style)
ax.plot(xx, yy, lw=1, color='black')
ax.plot(xx[1], yy[1], 'o', color='tab:red', markersize=3)

fig, ax = plt.subplots(figsize=(5, 4), constrained_layout=True)
ax.set_title('Join style')
for x, style in enumerate(['miter', 'round', 'bevel']):
ax.text(x, 5, style)
for y, angle in enumerate([20, 45, 60, 90, 120]):
plot_angle(ax, x, y, angle, style)
if x == 0:
ax.text(-1.3, y, f'{angle} degrees')
ax.set_xlim(-1.5, 2.75)
ax.set_ylim(-.5, 5.5)
ax.set_axis_off()
fig.show()


JoinStyle.input_description = "{" \
+ ", ".join([f"'{js.name}'" for js in JoinStyle]) \
+ "}"


class CapStyle(str, _AutoStringNameEnum):
r"""
Define how the two endpoints (caps) of an unclosed line are drawn.

How to draw the start and end points of lines that represent a closed curve
(i.e. that end in a `~.path.Path.CLOSEPOLY`) is controlled by the line's
`JoinStyle`. For all other lines, how the start and end points are drawn is
controlled by the *CapStyle*.

For a visual impression of each *CapStyle*, `view these docs online
<CapStyle>` or run `CapStyle.demo`.

**Supported values:**

.. rst-class:: value-list

'butt'
the line is squared off at its endpoint.
'projecting'
the line is squared off as in *butt*, but the filled in area
extends beyond the endpoint a distance of ``linewidth/2``.
'round'
like *butt*, but a semicircular cap is added to the end of the
line, of radius ``linewidth/2``.

.. plot::
:alt: Demo of possible CapStyle's

from matplotlib._enums import CapStyle
CapStyle.demo()

"""
butt = 'butt'
projecting = 'projecting'
round = 'round'

def __init__(self, s):
s = _deprecate_case_insensitive_join_cap(s)
Enum.__init__(self)

@staticmethod
def demo():
"""Demonstrate how each CapStyle looks for a thick line segment."""
import matplotlib.pyplot as plt

fig = plt.figure(figsize=(4, 1.2))
ax = fig.add_axes([0, 0, 1, 0.8])
ax.set_title('Cap style')

for x, style in enumerate(['butt', 'round', 'projecting']):
ax.text(x+0.25, 0.85, style, ha='center')
xx = [x, x+0.5]
yy = [0, 0]
ax.plot(xx, yy, lw=12, color='tab:blue', solid_capstyle=style)
ax.plot(xx, yy, lw=1, color='black')
ax.plot(xx, yy, 'o', color='tab:red', markersize=3)
ax.text(2.25, 0.55, '(default)', ha='center')

ax.set_ylim(-.5, 1.5)
ax.set_axis_off()
fig.show()


CapStyle.input_description = "{" \
+ ", ".join([f"'{cs.name}'" for cs in CapStyle]) \
+ "}"

docstring.interpd.update({'JoinStyle': JoinStyle.input_description,
'CapStyle': CapStyle.input_description})
Loading