From e487b1c0d91f01e51c3203666b1a5e19d7c43920 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Fri, 23 Jul 2021 08:57:10 +0200 Subject: [PATCH 01/22] Add user supplied transforms and join/cap styles Improvement was done in instantiating new instance utilizing deep copy should preserve immutability of MarkerStyle members (e.g. Path, or Transform). --- lib/matplotlib/markers.py | 50 ++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 8fbbb71818a4..644ab2a1aba7 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -127,6 +127,7 @@ .. |m36| image:: /_static/markers/m36.png .. |m37| image:: /_static/markers/m37.png """ +import copy from collections.abc import Sized import inspect @@ -221,7 +222,8 @@ class MarkerStyle: _unset = object() # For deprecation of MarkerStyle(). - def __init__(self, marker=_unset, fillstyle=None): + def __init__(self, marker=_unset, fillstyle=None, + transform=None, capstyle=None, joinstyle=None): """ Parameters ---------- @@ -236,6 +238,9 @@ def __init__(self, marker=_unset, fillstyle=None): One of 'full', 'left', 'right', 'bottom', 'top', 'none'. """ self._marker_function = None + self._user_transform = transform + self._user_capstyle = capstyle + self._user_joinstyle = joinstyle self._set_fillstyle(fillstyle) # Remove _unset and signature rewriting after deprecation elapses. if marker is self._unset: @@ -265,7 +270,7 @@ def _recache(self): self._alt_transform = None self._snap_threshold = None self._joinstyle = JoinStyle.round - self._capstyle = CapStyle.butt + self._capstyle = self._user_capstyle or CapStyle.butt # Initial guess: Assume the marker is filled unless the fillstyle is # set to 'none'. The marker function will override this for unfilled # markers. @@ -342,7 +347,8 @@ def _set_marker(self, marker): self._marker_function = getattr( self, '_set_' + self.markers[marker]) elif isinstance(marker, MarkerStyle): - self.__dict__.update(marker.__dict__) + self.__dict__ = copy.deepcopy(marker.__dict__) + else: try: Path(marker) @@ -369,7 +375,10 @@ def get_transform(self): Return the transform to be applied to the `.Path` from `MarkerStyle.get_path()`. """ - return self._transform.frozen() + if self._user_transform is not None: + return (self._transform + self._user_transform).frozen() + else: + return self._transform.frozen() def get_alt_path(self): """ @@ -385,7 +394,10 @@ def get_alt_transform(self): Return the transform to be applied to the `.Path` from `MarkerStyle.get_alt_path()`. """ - return self._alt_transform.frozen() + if self._user_transform is not None: + return (self._alt_transform + self._user_transform).frozen() + else: + return self._alt_transform.frozen() def get_snap_threshold(self): return self._snap_threshold @@ -413,14 +425,14 @@ def _set_tuple_marker(self): symstyle = marker[1] if symstyle == 0: self._path = Path.unit_regular_polygon(numsides) - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter elif symstyle == 1: self._path = Path.unit_regular_star(numsides) - self._joinstyle = JoinStyle.bevel + self._joinstyle = self._user_joinstyle or JoinStyle.bevel elif symstyle == 2: self._path = Path.unit_regular_asterisk(numsides) self._filled = False - self._joinstyle = JoinStyle.bevel + self._joinstyle = self._user_joinstyle or JoinStyle.bevel else: raise ValueError(f"Unexpected tuple marker: {marker}") self._transform = Affine2D().scale(0.5).rotate_deg(rotation) @@ -521,7 +533,7 @@ def _set_triangle(self, rot, skip): self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_triangle_up(self): return self._set_triangle(0.0, 0) @@ -551,7 +563,7 @@ def _set_square(self): self._transform.rotate_deg(rotate) self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_diamond(self): self._transform = Affine2D().translate(-0.5, -0.5).rotate_deg(45) @@ -565,7 +577,7 @@ def _set_diamond(self): rotate = {'right': 0, 'top': 90, 'left': 180, 'bottom': 270}[fs] self._transform.rotate_deg(rotate) self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_thin_diamond(self): self._set_diamond() @@ -592,7 +604,7 @@ def _set_pentagon(self): }[self.get_fillstyle()] self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_star(self): self._transform = Affine2D().scale(0.5) @@ -614,7 +626,7 @@ def _set_star(self): }[self.get_fillstyle()] self._alt_transform = self._transform - self._joinstyle = JoinStyle.bevel + self._joinstyle = self._user_joinstyle or JoinStyle.bevel def _set_hexagon1(self): self._transform = Affine2D().scale(0.5) @@ -638,7 +650,7 @@ def _set_hexagon1(self): }[self.get_fillstyle()] self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_hexagon2(self): self._transform = Affine2D().scale(0.5).rotate_deg(30) @@ -664,7 +676,7 @@ def _set_hexagon2(self): }[self.get_fillstyle()] self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_octagon(self): self._transform = Affine2D().scale(0.5) @@ -685,7 +697,7 @@ def _set_octagon(self): {'left': 0, 'bottom': 90, 'right': 180, 'top': 270}[fs]) self._alt_transform = self._transform.frozen().rotate_deg(180.0) - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter _line_marker_path = Path([[0.0, -1.0], [0.0, 1.0]]) @@ -759,7 +771,7 @@ def _set_caretdown(self): self._snap_threshold = 3.0 self._filled = False self._path = self._caret_path - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_caretup(self): self._set_caretdown() @@ -825,7 +837,7 @@ def _set_x(self): def _set_plus_filled(self): self._transform = Affine2D() self._snap_threshold = 5.0 - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter if not self._half_fill(): self._path = self._plus_filled_path else: @@ -849,7 +861,7 @@ def _set_plus_filled(self): def _set_x_filled(self): self._transform = Affine2D() self._snap_threshold = 5.0 - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter if not self._half_fill(): self._path = self._x_filled_path else: From 37859ea33869d86c4e57aaa3512a705092e774be Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Thu, 26 Aug 2021 20:40:53 +0200 Subject: [PATCH 02/22] Avoid reinstanting MarkerStyle in lines.py --- lib/matplotlib/lines.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index ea3b98b63208..f1efd125a4ed 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -370,7 +370,10 @@ def __init__(self, xdata, ydata, self.set_color(color) if marker is None: marker = 'none' # Default. - self._marker = MarkerStyle(marker, fillstyle) + if not isinstance(marker, MarkerStyle): + self._marker = MarkerStyle(marker, fillstyle) + else: + self._marker = marker self._markevery = None self._markersize = None From 81536320d4a702db9376782ee7ac6b5a6f43971c Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sun, 5 Sep 2021 23:30:26 +0200 Subject: [PATCH 03/22] Update dosctring. --- lib/matplotlib/markers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 644ab2a1aba7..b4413bbe3ba0 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -236,6 +236,18 @@ def __init__(self, marker=_unset, fillstyle=None, fillstyle : str, default: :rc:`markers.fillstyle` One of 'full', 'left', 'right', 'bottom', 'top', 'none'. + + transform : Affine2D, default: None + User supplied transformation that will be combined with the + native transformation of selected marker. + + capstyle : CapStyle, default: None + User supplied cap style that will override the default cap + style of selected marker. + + joinstyle : JoinStyle, default: None + User supplied join style that will override the default cap + style of selected marker. """ self._marker_function = None self._user_transform = transform From 4469006fb4b8720876b938023561233dd9588993 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Mon, 6 Sep 2021 22:56:14 +0200 Subject: [PATCH 04/22] Add marker.transformed + test --- lib/matplotlib/markers.py | 51 +++++++++++++++++++++++++++-- lib/matplotlib/tests/test_marker.py | 11 +++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index b4413bbe3ba0..5652382c945a 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -236,7 +236,7 @@ def __init__(self, marker=_unset, fillstyle=None, fillstyle : str, default: :rc:`markers.fillstyle` One of 'full', 'left', 'right', 'bottom', 'top', 'none'. - + transform : Affine2D, default: None User supplied transformation that will be combined with the native transformation of selected marker. @@ -319,10 +319,10 @@ def _set_fillstyle(self, fillstyle): self._recache() def get_joinstyle(self): - return self._joinstyle + return self._user_joinstyle or self._joinstyle def get_capstyle(self): - return self._capstyle + return self._user_capstyle or self._capstyle def get_marker(self): return self._marker @@ -414,6 +414,51 @@ def get_alt_transform(self): def get_snap_threshold(self): return self._snap_threshold + def get_user_transform(self): + """Return user supplied part of marker transform.""" + if self._user_transform is not None: + return self._user_transform.frozen() + + def transformed(self, transform:Affine2D): + """ + Return new marker with combined transformation. + + Parameters + ---------- + transform : Affine2D, default: None + - transform will be combined with current user supplied transform. + """ + new_marker = MarkerStyle(self) + if new_marker._user_transform is not None: + new_marker._user_transform += transform + else: + new_marker._user_transform = transform + return new_marker + + def rotated(self, deg=None, rad=None): + """ + Return new marker rotated by specified angle. + + Parameters + ---------- + deg : float, default: None + + rad : float, default: None + """ + if not ((deg is None) ^ (rad is None)): + raise Exception("Only one of deg or rad shall be used.") + + _transform = self._user_transform or Affine2D() + + if deg is not None: + _transform = _transform.rotate_deg(deg) + + if rad is not None: + _transform = _transform.rotate(rad) + + new_marker = MarkerStyle(self) + new_marker._user_transform = _transform + def _set_nothing(self): self._filled = False diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index 75681b0e1a9b..11c9564a9218 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -4,6 +4,7 @@ from matplotlib._api.deprecation import MatplotlibDeprecationWarning from matplotlib.path import Path from matplotlib.testing.decorators import check_figures_equal +from matplotlib.transforms import Affine2D import pytest @@ -204,3 +205,13 @@ def test_marker_clipping(fig_ref, fig_test): ax_test.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow)) ax_ref.axis('off') ax_test.axis('off') + +@pytest.mark.parametrize("marker,transform,expected", [ + (markers.MarkerStyle("o"), Affine2D().translate(1,1), Affine2D().translate(1,1)), + (markers.MarkerStyle("o", transform=Affine2D().translate(1,1)), Affine2D().translate(1,1), Affine2D().translate(2,2)), +]) +def test_marker_transformed(marker, transform, expected): + new_marker = marker.transformed(transform) + assert new_marker is not marker + assert new_marker.get_user_transform() == expected + assert marker.get_user_transform() is not new_marker.get_user_transform() \ No newline at end of file From 4ea2b913da2932e08a3d8eea4ad0ff63290d4306 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Fri, 10 Sep 2021 23:10:54 +0200 Subject: [PATCH 05/22] Add affine transform primitives to markers Added, translated, scaled, and rotated methods with test. --- lib/matplotlib/markers.py | 61 ++++++++++++++++++++++++----- lib/matplotlib/tests/test_marker.py | 41 +++++++++++++++++-- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 5652382c945a..90532104f8a9 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -419,7 +419,7 @@ def get_user_transform(self): if self._user_transform is not None: return self._user_transform.frozen() - def transformed(self, transform:Affine2D): + def transformed(self, transform: Affine2D): """ Return new marker with combined transformation. @@ -442,22 +442,65 @@ def rotated(self, deg=None, rad=None): Parameters ---------- deg : float, default: None + - use this parameter to specify rotation angle in degrees. rad : float, default: None + - use this parameter to specify rotation angle in radians. + + Note: you must specify exactly one of deg or rad. """ if not ((deg is None) ^ (rad is None)): - raise Exception("Only one of deg or rad shall be used.") - - _transform = self._user_transform or Affine2D() + raise ValueError("Exactly one of deg or rad shall be used.") + + new_marker = MarkerStyle(self) + if new_marker._user_transform is None: + new_marker._user_transform = Affine2D() - if deg is not None: - _transform = _transform.rotate_deg(deg) - + if deg is not None: + new_marker._user_transform.rotate_deg(deg) if rad is not None: - _transform = _transform.rotate(rad) + new_marker._user_transform.rotate(rad) + + return new_marker + + def scaled(self, sx, sy=None): + """ + Return new marker scaled by specified scale factors. + + If *sy* is None, the same scale is applied in both the *x*- and + *y*-directions. + + Parameters + ---------- + sx : float + - *x*-direction scaling factor. + + sy : float, default: None + - *y*-direction scaling factor. + """ + if sy is None: + sy = sx + + new_marker = MarkerStyle(self) + _transform = new_marker._user_transform or Affine2D() + new_marker._user_transform = _transform.scale(sx, sy) + return new_marker + + def translated(self, tx, ty): + """ + Return new marker translated by tx and ty. + Parameters + ---------- + tx : float + + ty : float + + """ new_marker = MarkerStyle(self) - new_marker._user_transform = _transform + _transform = new_marker._user_transform or Affine2D() + new_marker._user_transform = _transform.translate(tx, ty) + return new_marker def _set_nothing(self): self._filled = False diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index 11c9564a9218..db2aaa768e8f 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -206,12 +206,47 @@ def test_marker_clipping(fig_ref, fig_test): ax_ref.axis('off') ax_test.axis('off') + @pytest.mark.parametrize("marker,transform,expected", [ - (markers.MarkerStyle("o"), Affine2D().translate(1,1), Affine2D().translate(1,1)), - (markers.MarkerStyle("o", transform=Affine2D().translate(1,1)), Affine2D().translate(1,1), Affine2D().translate(2,2)), + (markers.MarkerStyle("o"), Affine2D().translate(1, 1), + Affine2D().translate(1, 1)), + (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)), + Affine2D().translate(1, 1), Affine2D().translate(2, 2)), + # (markers.MarkerStyle("$|||$", transform=Affine2D().translate(1, 1)), + # Affine2D().translate(1, 1), Affine2D().translate(2, 2)), + (markers.MarkerStyle( + markers.TICKLEFT, transform=Affine2D().translate(1, 1)), + Affine2D().translate(1, 1), Affine2D().translate(2, 2)), ]) def test_marker_transformed(marker, transform, expected): new_marker = marker.transformed(transform) assert new_marker is not marker assert new_marker.get_user_transform() == expected - assert marker.get_user_transform() is not new_marker.get_user_transform() \ No newline at end of file + assert marker._user_transform is not new_marker._user_transform + + +def test_marker_rotated_invalid(): + marker = markers.MarkerStyle("o") + with pytest.raises(ValueError): + new_marker = marker.rotated() + new_marker = marker.rotated(deg=10, rad=10) + + +@pytest.mark.parametrize("marker,deg,rad,expected", [ + (markers.MarkerStyle("o"), 10, None, Affine2D().rotate_deg(10)), + (markers.MarkerStyle("o"), None, 0.01, Affine2D().rotate(0.01)), + (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)), + 10, None, Affine2D().translate(1, 1).rotate_deg(10)), + (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)), + None, 0.01, Affine2D().translate(1, 1).rotate(0.01)), + # (markers.MarkerStyle("$|||$", transform=Affine2D().translate(1, 1)), + # 10, None, Affine2D().translate(1, 1).rotate_deg(10)), + (markers.MarkerStyle( + markers.TICKLEFT, transform=Affine2D().translate(1, 1)), + 10, None, Affine2D().translate(1, 1).rotate_deg(10)), +]) +def test_marker_rotated_deg(marker, deg, rad, expected): + new_marker = marker.rotated(deg=deg, rad=rad) + assert new_marker is not marker + assert new_marker.get_user_transform() == expected + assert marker._user_transform is not new_marker._user_transform From 7f6d1bece34f6dbbb8ab917e828acc2edda34425 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sat, 11 Sep 2021 20:20:54 +0200 Subject: [PATCH 06/22] Tweaking docstrings in MarkerStyle. --- lib/matplotlib/markers.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 90532104f8a9..fc639fd6fbd8 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -426,7 +426,7 @@ def transformed(self, transform: Affine2D): Parameters ---------- transform : Affine2D, default: None - - transform will be combined with current user supplied transform. + Transform will be combined with current user supplied transform. """ new_marker = MarkerStyle(self) if new_marker._user_transform is not None: @@ -442,12 +442,12 @@ def rotated(self, deg=None, rad=None): Parameters ---------- deg : float, default: None - - use this parameter to specify rotation angle in degrees. + Use this parameter to specify rotation angle in degrees. rad : float, default: None - - use this parameter to specify rotation angle in radians. + Use this parameter to specify rotation angle in radians. - Note: you must specify exactly one of deg or rad. + .. note:: You must specify exactly one of deg or rad. """ if not ((deg is None) ^ (rad is None)): raise ValueError("Exactly one of deg or rad shall be used.") @@ -473,10 +473,9 @@ def scaled(self, sx, sy=None): Parameters ---------- sx : float - - *x*-direction scaling factor. - + *X*-direction scaling factor. sy : float, default: None - - *y*-direction scaling factor. + *Y*-direction scaling factor. """ if sy is None: sy = sx @@ -493,9 +492,9 @@ def translated(self, tx, ty): Parameters ---------- tx : float - + Coordinate for translation in *x*-direction. ty : float - + Coordinate for translation in *y*-direction. """ new_marker = MarkerStyle(self) _transform = new_marker._user_transform or Affine2D() From 2729fe43217ea810c33131eb640aac8c5c45abd3 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sun, 12 Sep 2021 17:05:03 +0200 Subject: [PATCH 07/22] Update MarkerStyle module documentation --- lib/matplotlib/markers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index fc639fd6fbd8..b380e1203484 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -82,6 +82,13 @@ plt.plot([1, 2, 3], marker=11) plt.plot([1, 2, 3], marker=matplotlib.markers.CARETDOWNBASE) +Markers have some reasonable default settings for join and cap styles. +However, those can be overriden when creating a new instance of MarkerStyle. +Furthermore, the marker shape can be modified by supplying +`~matplotlib .transforms.Transform` during creation. +Some markers are created as internally rotated shapes (e.g. triangles). +For such cases, both internal and user supplied transforms are combined. + Examples showing the use of markers: * :doc:`/gallery/lines_bars_and_markers/marker_reference` From 435799533a9a2a741d0b04aad23b64730263ebd7 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sun, 12 Sep 2021 17:05:38 +0200 Subject: [PATCH 08/22] Update marker Reference documentation --- .../marker_reference.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/examples/lines_bars_and_markers/marker_reference.py b/examples/lines_bars_and_markers/marker_reference.py index 98af2519124c..d1d675c5c268 100644 --- a/examples/lines_bars_and_markers/marker_reference.py +++ b/examples/lines_bars_and_markers/marker_reference.py @@ -19,8 +19,10 @@ .. redirect-from:: /gallery/shapes_and_collections/marker_path """ +from matplotlib.markers import MarkerStyle import matplotlib.pyplot as plt from matplotlib.lines import Line2D +from matplotlib.transforms import Affine2D text_style = dict(horizontalalignment='right', verticalalignment='center', @@ -159,3 +161,98 @@ def split_list(a_list): format_axes(ax) plt.show() + +############################################################################### +# Advanced marker modifications with transform +# ============================================ +# +# All markers can be modified by a user transform in MarkerStyle constructor. +# Supplied transform is combined with the default transforms needed for +# selected marker shape (e.g. caret up, caret down). Following example shows +# how user supplied rotation applies to several marker shapes. + +common_style = {k: v for k, v in filled_marker_style.items() if v != 'marker'} +angles = [0, 10, 20, 30, 45, 60, 90] + +fig, ax = plt.subplots() +fig.suptitle('Rotated markers', fontsize=14) + +ax.text(-0.5, 0, 'Filled marker', **text_style) +for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + ax.plot(x, 0, marker=MarkerStyle('o', 'left', t), **common_style) + +ax.text(-0.5, 1, 'Un-filled marker', **text_style) +for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + ax.plot(x, 1, marker=MarkerStyle('1', 'left', t), **common_style) + +ax.text(-0.5, 2, 'Equation marker', **text_style) +for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + eq = r'$\frac{1}{x}$' + ax.plot(x, 2, marker=MarkerStyle(eq, 'left', t), **common_style) + +for x, theta in enumerate(angles): + ax.text(x, 2.5, f"{theta}°", horizontalalignment="center") +format_axes(ax) + +plt.show() + +############################################################################### +# Setting marker cap style and join style +# ======================================= +# +# All markers have predefined cap style and join style, but this can be +# overriden during creation of MarkerStyle. Follwing example show how to +# change the cap style and how different styles look. + +from matplotlib.markers import JoinStyle, CapStyle + +marker_inner = dict(markersize=35, + markerfacecolor='tab:blue', + markerfacecoloralt='lightsteelblue', + markeredgecolor='brown', + markeredgewidth=8, + ) + +marker_outer = dict(markersize=35, + markerfacecolor='tab:blue', + markerfacecoloralt='lightsteelblue', + markeredgecolor='white', + markeredgewidth=1, + ) + +fig, ax = plt.subplots() +fig.suptitle('Marker CapStyle', fontsize=14) +fig.subplots_adjust(left=0.1) + +for y, cap_style in enumerate([None, *CapStyle]): + ax.text(-0.5, y, cap_style.name, **text_style) + for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + m = MarkerStyle('1', transform=t, capstyle=cap_style) + ax.plot(x, y, marker=m, **marker_inner) + ax.plot(x, y, marker=m, **marker_outer) + ax.text(x, len(CapStyle) - .5, f'{theta}°', ha='center') +format_axes(ax) +plt.show() + +############################################################################### +# Follwing example show how to change the join style and how different styles +# look. + +fig, ax = plt.subplots() +fig.suptitle('Marker JoinStyle', fontsize=14) +fig.subplots_adjust(left=0.05) + +for y, join_style in enumerate(JoinStyle): + ax.text(-0.5, y, join_style.name, **text_style) + for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + m = MarkerStyle('*', transform=t, joinstyle=join_style) + ax.plot(x, y, marker=m, **marker_inner) + ax.text(x, len(JoinStyle) - .5, f'{theta}°', ha='center') +format_axes(ax) + +plt.show() From b1bbdff12758e8e0a30841ba075cdd6c9c2485c6 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sun, 12 Sep 2021 17:06:03 +0200 Subject: [PATCH 09/22] Add whats new for MarkerStyle features --- .../next_whats_new/extending_MarkerStyle.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 doc/users/next_whats_new/extending_MarkerStyle.rst diff --git a/doc/users/next_whats_new/extending_MarkerStyle.rst b/doc/users/next_whats_new/extending_MarkerStyle.rst new file mode 100644 index 000000000000..06479a7badf8 --- /dev/null +++ b/doc/users/next_whats_new/extending_MarkerStyle.rst @@ -0,0 +1,19 @@ +New customization of MarkerStyle +-------------------------------- + +New MarkerStyle parameters allow control of join style and cap style. +The appearance of individual markers can be further controlled by +transform supplied during creation. + +.. plot:: + :include-source: + import matplotlib.pyplot as plt + from matplotlib.markers import MarkerStyle + from matplotlib.transforms import Affine2D + fig, ax = plt.subplots(figsize=(6, 1)) + fig.suptitle('New markers', fontsize=14) + for col, (size, rot) in enumerate(zip([2, 5, 10], [0, 45, 90])): + t = Affine2D().rotate_deg(rot).scale(size) + ax.plot(col, 0, marker=MarkerStyle("*", transform=t)) + ax.axis("off") + ax.set_xlim(-0.1, 2.4) From 9ac3ad2f6e5a14ab12a1dabf975ba71fa466f42f Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sun, 12 Sep 2021 19:27:36 +0200 Subject: [PATCH 10/22] Add new example with advanced MarkerStyle mapping --- .../multivariate_marker_plot.py | 45 +++++++++++++++++++ lib/matplotlib/markers.py | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 examples/lines_bars_and_markers/multivariate_marker_plot.py diff --git a/examples/lines_bars_and_markers/multivariate_marker_plot.py b/examples/lines_bars_and_markers/multivariate_marker_plot.py new file mode 100644 index 000000000000..274a1113fbc1 --- /dev/null +++ b/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -0,0 +1,45 @@ +""" +============================================== +Mapping marker properties to multivariate data +============================================== + +This example shows how to use different properties of markers to plot +multivariate datasets. Following example shows an illustrative case of +plotting success of baseball throw as an smiley face with size mapped to +the skill of thrower, rotation mapped to the take-off angle, and thrust +to the color of the marker. +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.markers import MarkerStyle +from matplotlib.transforms import Affine2D +from matplotlib.textpath import TextPath +from matplotlib.colors import Normalize + +SUCCESS_SYMBOLS = [ + TextPath((0,0), "☹"), + TextPath((0,0), "😒"), + TextPath((0,0), "☺"), +] + +N = 25 +np.random.seed(42) +skills = np.random.uniform(5, 80, size=N) * 0.1 + 5 +takeoff_angles = np.random.normal(0, 90, N) +thrusts = np.random.uniform(size=N) +successfull = np.random.randint(0, 3, size=N) +positions = np.random.normal(size=(N, 2)) * 5 +data = zip(skills, takeoff_angles, thrusts, successfull, positions) + +cmap = plt.cm.get_cmap("plasma") +fig, ax = plt.subplots() +fig.suptitle("Throwing success", size=14) +for skill, takeoff, thrust, mood, pos in data: + t = Affine2D().scale(skill).rotate_deg(takeoff) + m = MarkerStyle(SUCCESS_SYMBOLS[mood], transform=t) + ax.plot(pos[0], pos[1], marker=m, color=cmap(thrust)) +fig.colorbar(plt.cm.ScalarMappable(norm=Normalize(0, 1), cmap=cmap), + ax=ax, label="Normalized Thrust [a.u.]") +ax.set_xlabel("X position [m]") +ax.set_ylabel("Y position [m]") diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index b380e1203484..86b3492d8776 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -93,7 +93,7 @@ * :doc:`/gallery/lines_bars_and_markers/marker_reference` * :doc:`/gallery/lines_bars_and_markers/scatter_star_poly` - +* :doc:`/gallery/lines_bars_and_markers/multivariate_marker_plot` .. |m00| image:: /_static/markers/m00.png .. |m01| image:: /_static/markers/m01.png From 41808430ace77c5dfdbb815c1d2e26ad3c1e8181 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sun, 12 Sep 2021 23:40:33 +0200 Subject: [PATCH 11/22] Fix errors discovered by CI --- doc/users/next_whats_new/extending_MarkerStyle.rst | 3 ++- examples/lines_bars_and_markers/marker_reference.py | 6 +++--- .../lines_bars_and_markers/multivariate_marker_plot.py | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/users/next_whats_new/extending_MarkerStyle.rst b/doc/users/next_whats_new/extending_MarkerStyle.rst index 06479a7badf8..fbe0fadd7f0c 100644 --- a/doc/users/next_whats_new/extending_MarkerStyle.rst +++ b/doc/users/next_whats_new/extending_MarkerStyle.rst @@ -6,7 +6,8 @@ The appearance of individual markers can be further controlled by transform supplied during creation. .. plot:: - :include-source: + :include-source: true + import matplotlib.pyplot as plt from matplotlib.markers import MarkerStyle from matplotlib.transforms import Affine2D diff --git a/examples/lines_bars_and_markers/marker_reference.py b/examples/lines_bars_and_markers/marker_reference.py index d1d675c5c268..f82ec36035e6 100644 --- a/examples/lines_bars_and_markers/marker_reference.py +++ b/examples/lines_bars_and_markers/marker_reference.py @@ -171,7 +171,7 @@ def split_list(a_list): # selected marker shape (e.g. caret up, caret down). Following example shows # how user supplied rotation applies to several marker shapes. -common_style = {k: v for k, v in filled_marker_style.items() if v != 'marker'} +common_style = {k: v for k, v in filled_marker_style.items() if k != 'marker'} angles = [0, 10, 20, 30, 45, 60, 90] fig, ax = plt.subplots() @@ -227,7 +227,7 @@ def split_list(a_list): fig.suptitle('Marker CapStyle', fontsize=14) fig.subplots_adjust(left=0.1) -for y, cap_style in enumerate([None, *CapStyle]): +for y, cap_style in enumerate(CapStyle): ax.text(-0.5, y, cap_style.name, **text_style) for x, theta in enumerate(angles): t = Affine2D().rotate_deg(theta) @@ -240,7 +240,7 @@ def split_list(a_list): ############################################################################### # Follwing example show how to change the join style and how different styles -# look. +# looks like. fig, ax = plt.subplots() fig.suptitle('Marker JoinStyle', fontsize=14) diff --git a/examples/lines_bars_and_markers/multivariate_marker_plot.py b/examples/lines_bars_and_markers/multivariate_marker_plot.py index 274a1113fbc1..51d7d4bca30e 100644 --- a/examples/lines_bars_and_markers/multivariate_marker_plot.py +++ b/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -18,9 +18,9 @@ from matplotlib.colors import Normalize SUCCESS_SYMBOLS = [ - TextPath((0,0), "☹"), - TextPath((0,0), "😒"), - TextPath((0,0), "☺"), + TextPath((0, 0), "☹"), + TextPath((0, 0), "😒"), + TextPath((0, 0), "☺"), ] N = 25 @@ -40,6 +40,6 @@ m = MarkerStyle(SUCCESS_SYMBOLS[mood], transform=t) ax.plot(pos[0], pos[1], marker=m, color=cmap(thrust)) fig.colorbar(plt.cm.ScalarMappable(norm=Normalize(0, 1), cmap=cmap), - ax=ax, label="Normalized Thrust [a.u.]") + ax=ax, label="Normalized Thrust [a.u.]") ax.set_xlabel("X position [m]") ax.set_ylabel("Y position [m]") From 905eba488f9d2f0db420bd710b4fdc465397a2d8 Mon Sep 17 00:00:00 2001 From: Jakub Klus <48711526+deep-jkl@users.noreply.github.com> Date: Mon, 13 Sep 2021 08:07:33 +0200 Subject: [PATCH 12/22] Fix typo in markers api documentation. Apply suggestion from @timhoffm, update lib/matplotlib/markers.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/markers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 86b3492d8776..ad06879532f6 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -85,7 +85,7 @@ Markers have some reasonable default settings for join and cap styles. However, those can be overriden when creating a new instance of MarkerStyle. Furthermore, the marker shape can be modified by supplying -`~matplotlib .transforms.Transform` during creation. +`~matplotlib.transforms.Transform` during creation. Some markers are created as internally rotated shapes (e.g. triangles). For such cases, both internal and user supplied transforms are combined. From 3fc6140998d3a8cf130d53e161c0b2b4ff852a18 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Wed, 15 Sep 2021 00:32:03 +0200 Subject: [PATCH 13/22] Improve test for invalid rotated inputs --- lib/matplotlib/markers.py | 2 +- lib/matplotlib/tests/test_marker.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index ad06879532f6..2a51d8fa5e9a 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -456,7 +456,7 @@ def rotated(self, deg=None, rad=None): .. note:: You must specify exactly one of deg or rad. """ - if not ((deg is None) ^ (rad is None)): + if not (deg is None) ^ (rad is None): raise ValueError("Exactly one of deg or rad shall be used.") new_marker = MarkerStyle(self) diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index db2aaa768e8f..e2b18354f34a 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -229,6 +229,7 @@ def test_marker_rotated_invalid(): marker = markers.MarkerStyle("o") with pytest.raises(ValueError): new_marker = marker.rotated() + with pytest.raises(ValueError): new_marker = marker.rotated(deg=10, rad=10) From 04a3e7046b0ae8c54f3fbd70b35d975005731a03 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Wed, 15 Sep 2021 00:39:35 +0200 Subject: [PATCH 14/22] Add tests for MarkerStyle scaled and translated --- lib/matplotlib/tests/test_marker.py | 38 +++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index e2b18354f34a..7925f22f54f7 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -229,7 +229,7 @@ def test_marker_rotated_invalid(): marker = markers.MarkerStyle("o") with pytest.raises(ValueError): new_marker = marker.rotated() - with pytest.raises(ValueError): + with pytest.raises(ValueError): new_marker = marker.rotated(deg=10, rad=10) @@ -246,8 +246,42 @@ def test_marker_rotated_invalid(): markers.TICKLEFT, transform=Affine2D().translate(1, 1)), 10, None, Affine2D().translate(1, 1).rotate_deg(10)), ]) -def test_marker_rotated_deg(marker, deg, rad, expected): +def test_marker_rotated(marker, deg, rad, expected): new_marker = marker.rotated(deg=deg, rad=rad) assert new_marker is not marker assert new_marker.get_user_transform() == expected assert marker._user_transform is not new_marker._user_transform + + +def test_marker_translated(): + marker = markers.MarkerStyle("1") + new_marker = marker.translated(1, 1) + assert new_marker is not marker + assert new_marker.get_user_transform() == Affine2D().translate(1, 1) + assert marker._user_transform is not new_marker._user_transform + + marker = markers.MarkerStyle("1", transform=Affine2D().translate(1, 1)) + new_marker = marker.translated(1, 1) + assert new_marker is not marker + assert new_marker.get_user_transform() == Affine2D().translate(2, 2) + assert marker._user_transform is not new_marker._user_transform + + +def test_marker_scaled(): + marker = markers.MarkerStyle("1") + new_marker = marker.scaled(2) + assert new_marker is not marker + assert new_marker.get_user_transform() == Affine2D().scale(2) + assert marker._user_transform is not new_marker._user_transform + + new_marker = marker.scaled(2, 3) + assert new_marker is not marker + assert new_marker.get_user_transform() == Affine2D().scale(2, 3) + assert marker._user_transform is not new_marker._user_transform + + marker = markers.MarkerStyle("1", transform=Affine2D().translate(1, 1)) + new_marker = marker.scaled(2) + assert new_marker is not marker + expected = Affine2D().translate(1, 1).scale(2) + assert new_marker.get_user_transform() == expected + assert marker._user_transform is not new_marker._user_transform From af6b89ad58025d24fe73d8292a8c5e08f4d330bd Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Wed, 15 Sep 2021 22:09:27 +0200 Subject: [PATCH 15/22] Add function for optimized None+transform --- lib/matplotlib/markers.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 2a51d8fa5e9a..e8fafd909e19 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -154,6 +154,14 @@ _empty_path = Path(np.empty((0, 2))) +def _fast_transform_combine(t1, t2): + """Combine two transformations where the second one can be None.""" + if t2 is None: + return t1.frozen() + else: + return (t1 + t2).frozen() + + class MarkerStyle: """ A class representing marker types. @@ -394,10 +402,7 @@ def get_transform(self): Return the transform to be applied to the `.Path` from `MarkerStyle.get_path()`. """ - if self._user_transform is not None: - return (self._transform + self._user_transform).frozen() - else: - return self._transform.frozen() + return _fast_transform_combine(self._transform, self._user_transform) def get_alt_path(self): """ @@ -413,10 +418,8 @@ def get_alt_transform(self): Return the transform to be applied to the `.Path` from `MarkerStyle.get_alt_path()`. """ - if self._user_transform is not None: - return (self._alt_transform + self._user_transform).frozen() - else: - return self._alt_transform.frozen() + return _fast_transform_combine(self._alt_transform, + self._user_transform) def get_snap_threshold(self): return self._snap_threshold From c01e8f1b5e1786b7be1159e92833496712ac7e89 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Wed, 15 Sep 2021 22:11:05 +0200 Subject: [PATCH 16/22] Remove ambiguous translated primitive for MarkerStyle --- lib/matplotlib/markers.py | 16 ---------------- lib/matplotlib/tests/test_marker.py | 14 -------------- 2 files changed, 30 deletions(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index e8fafd909e19..6a839d040495 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -495,22 +495,6 @@ def scaled(self, sx, sy=None): new_marker._user_transform = _transform.scale(sx, sy) return new_marker - def translated(self, tx, ty): - """ - Return new marker translated by tx and ty. - - Parameters - ---------- - tx : float - Coordinate for translation in *x*-direction. - ty : float - Coordinate for translation in *y*-direction. - """ - new_marker = MarkerStyle(self) - _transform = new_marker._user_transform or Affine2D() - new_marker._user_transform = _transform.translate(tx, ty) - return new_marker - def _set_nothing(self): self._filled = False diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index 7925f22f54f7..f7f5d5614cac 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -253,20 +253,6 @@ def test_marker_rotated(marker, deg, rad, expected): assert marker._user_transform is not new_marker._user_transform -def test_marker_translated(): - marker = markers.MarkerStyle("1") - new_marker = marker.translated(1, 1) - assert new_marker is not marker - assert new_marker.get_user_transform() == Affine2D().translate(1, 1) - assert marker._user_transform is not new_marker._user_transform - - marker = markers.MarkerStyle("1", transform=Affine2D().translate(1, 1)) - new_marker = marker.translated(1, 1) - assert new_marker is not marker - assert new_marker.get_user_transform() == Affine2D().translate(2, 2) - assert marker._user_transform is not new_marker._user_transform - - def test_marker_scaled(): marker = markers.MarkerStyle("1") new_marker = marker.scaled(2) From 95d93d27a3e82a311d86e33007668c36e11cb745 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Wed, 15 Sep 2021 22:25:18 +0200 Subject: [PATCH 17/22] Add missing test for MarkerStyle.get_alt_transform --- lib/matplotlib/tests/test_marker.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index f7f5d5614cac..d3665d9ce09d 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -271,3 +271,9 @@ def test_marker_scaled(): expected = Affine2D().translate(1, 1).scale(2) assert new_marker.get_user_transform() == expected assert marker._user_transform is not new_marker._user_transform + + +def test_alt_transform(): + m1 = markers.MarkerStyle("o", "left") + m2 = markers.MarkerStyle("o", "left", Affine2D().rotate_deg(90)) + assert m1.get_alt_transform().rotate_deg(90) == m2.get_alt_transform() From 75a92d81edfb69c0da319ced1f9f1e8b590e4b1f Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Thu, 16 Sep 2021 23:02:53 +0200 Subject: [PATCH 18/22] Improve documentation outputs. --- examples/lines_bars_and_markers/marker_reference.py | 1 + examples/lines_bars_and_markers/multivariate_marker_plot.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/examples/lines_bars_and_markers/marker_reference.py b/examples/lines_bars_and_markers/marker_reference.py index f82ec36035e6..f751bfb975b8 100644 --- a/examples/lines_bars_and_markers/marker_reference.py +++ b/examples/lines_bars_and_markers/marker_reference.py @@ -197,6 +197,7 @@ def split_list(a_list): ax.text(x, 2.5, f"{theta}°", horizontalalignment="center") format_axes(ax) +fig.tight_layout() plt.show() ############################################################################### diff --git a/examples/lines_bars_and_markers/multivariate_marker_plot.py b/examples/lines_bars_and_markers/multivariate_marker_plot.py index 51d7d4bca30e..0a79026b2772 100644 --- a/examples/lines_bars_and_markers/multivariate_marker_plot.py +++ b/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -43,3 +43,5 @@ ax=ax, label="Normalized Thrust [a.u.]") ax.set_xlabel("X position [m]") ax.set_ylabel("Y position [m]") + +plt.show() From e4f845643b0989ea6604e64181c1802ddc151aae Mon Sep 17 00:00:00 2001 From: Jakub Klus <48711526+deep-jkl@users.noreply.github.com> Date: Tue, 28 Sep 2021 09:26:39 +0200 Subject: [PATCH 19/22] Update docstrings according to jklymak Apply suggestions from @jklymak code review Co-authored-by: Jody Klymak --- .../next_whats_new/extending_MarkerStyle.rst | 5 ++-- .../marker_reference.py | 14 ++++------ .../multivariate_marker_plot.py | 7 +++-- lib/matplotlib/markers.py | 27 +++++++------------ 4 files changed, 20 insertions(+), 33 deletions(-) diff --git a/doc/users/next_whats_new/extending_MarkerStyle.rst b/doc/users/next_whats_new/extending_MarkerStyle.rst index fbe0fadd7f0c..6e970d0738fe 100644 --- a/doc/users/next_whats_new/extending_MarkerStyle.rst +++ b/doc/users/next_whats_new/extending_MarkerStyle.rst @@ -1,9 +1,8 @@ New customization of MarkerStyle -------------------------------- -New MarkerStyle parameters allow control of join style and cap style. -The appearance of individual markers can be further controlled by -transform supplied during creation. +New MarkerStyle parameters allow control of join style and cap style, and for +the user to supply a transformation to be applied to the marker (e.g. a rotation). .. plot:: :include-source: true diff --git a/examples/lines_bars_and_markers/marker_reference.py b/examples/lines_bars_and_markers/marker_reference.py index f751bfb975b8..ca9d1a77470a 100644 --- a/examples/lines_bars_and_markers/marker_reference.py +++ b/examples/lines_bars_and_markers/marker_reference.py @@ -166,10 +166,8 @@ def split_list(a_list): # Advanced marker modifications with transform # ============================================ # -# All markers can be modified by a user transform in MarkerStyle constructor. -# Supplied transform is combined with the default transforms needed for -# selected marker shape (e.g. caret up, caret down). Following example shows -# how user supplied rotation applies to several marker shapes. +# Markers can be modified by passing a transform to the MarkerStyle constructor. +# Following example shows how a supplied rotation is applied to several marker shapes. common_style = {k: v for k, v in filled_marker_style.items() if k != 'marker'} angles = [0, 10, 20, 30, 45, 60, 90] @@ -204,9 +202,8 @@ def split_list(a_list): # Setting marker cap style and join style # ======================================= # -# All markers have predefined cap style and join style, but this can be -# overriden during creation of MarkerStyle. Follwing example show how to -# change the cap style and how different styles look. +# Markers have default cap and join styles, but these can be +# customized when creating a MarkerStyle. from matplotlib.markers import JoinStyle, CapStyle @@ -240,8 +237,7 @@ def split_list(a_list): plt.show() ############################################################################### -# Follwing example show how to change the join style and how different styles -# looks like. +# Modifying the join style: fig, ax = plt.subplots() fig.suptitle('Marker JoinStyle', fontsize=14) diff --git a/examples/lines_bars_and_markers/multivariate_marker_plot.py b/examples/lines_bars_and_markers/multivariate_marker_plot.py index 0a79026b2772..284b21571d59 100644 --- a/examples/lines_bars_and_markers/multivariate_marker_plot.py +++ b/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -4,10 +4,9 @@ ============================================== This example shows how to use different properties of markers to plot -multivariate datasets. Following example shows an illustrative case of -plotting success of baseball throw as an smiley face with size mapped to -the skill of thrower, rotation mapped to the take-off angle, and thrust -to the color of the marker. +multivariate datasets. Here we represent a successful baseball throw as a smiley +face with marker size mapped to the skill of thrower, marker rotation to the take-off angle, +and thrust to the marker color. """ import numpy as np diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 6a839d040495..1436a8434d60 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -82,13 +82,9 @@ plt.plot([1, 2, 3], marker=11) plt.plot([1, 2, 3], marker=matplotlib.markers.CARETDOWNBASE) -Markers have some reasonable default settings for join and cap styles. -However, those can be overriden when creating a new instance of MarkerStyle. -Furthermore, the marker shape can be modified by supplying -`~matplotlib.transforms.Transform` during creation. -Some markers are created as internally rotated shapes (e.g. triangles). -For such cases, both internal and user supplied transforms are combined. - +Markers join and cap styles can be customized by creating a new instance of MarkerStyle. +A MarkerStyle can also have a custom +`~matplotlib.transforms.Transform` allowing it to be arbitrarily rotated or offset. Examples showing the use of markers: * :doc:`/gallery/lines_bars_and_markers/marker_reference` @@ -253,16 +249,13 @@ def __init__(self, marker=_unset, fillstyle=None, One of 'full', 'left', 'right', 'bottom', 'top', 'none'. transform : Affine2D, default: None - User supplied transformation that will be combined with the - native transformation of selected marker. + Transform that will be combined with the native transform of the marker. capstyle : CapStyle, default: None - User supplied cap style that will override the default cap - style of selected marker. + Cap style that will override the default cap style of the marker. joinstyle : JoinStyle, default: None - User supplied join style that will override the default cap - style of selected marker. + Join style that will override the default join style of the marker. """ self._marker_function = None self._user_transform = transform @@ -431,7 +424,7 @@ def get_user_transform(self): def transformed(self, transform: Affine2D): """ - Return new marker with combined transformation. + Return a new version of this marker with the transform applied. Parameters ---------- @@ -447,15 +440,15 @@ def transformed(self, transform: Affine2D): def rotated(self, deg=None, rad=None): """ - Return new marker rotated by specified angle. + Return a new version of this marker rotated by specified angle. Parameters ---------- deg : float, default: None - Use this parameter to specify rotation angle in degrees. + Rotation angle in degrees. rad : float, default: None - Use this parameter to specify rotation angle in radians. + Rotation angle in radians. .. note:: You must specify exactly one of deg or rad. """ From 5aa5bbd071acd6285f43c387a6a8538ffa57520a Mon Sep 17 00:00:00 2001 From: Jakub Klus <48711526+deep-jkl@users.noreply.github.com> Date: Tue, 28 Sep 2021 09:31:55 +0200 Subject: [PATCH 20/22] Make MarkerStyle.rotated more concise Apply suggestions from code review Co-authored-by: Jody Klymak --- lib/matplotlib/markers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 1436a8434d60..a2189ea23d6a 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -438,7 +438,7 @@ def transformed(self, transform: Affine2D): new_marker._user_transform = transform return new_marker - def rotated(self, deg=None, rad=None): + def rotated(self, *, deg=None, rad=None): """ Return a new version of this marker rotated by specified angle. @@ -452,9 +452,10 @@ def rotated(self, deg=None, rad=None): .. note:: You must specify exactly one of deg or rad. """ - if not (deg is None) ^ (rad is None): - raise ValueError("Exactly one of deg or rad shall be used.") - + if deg is None and rad is None: + raise ValueError('One of deg or rad is required') + if deg is not None and rad is not None: + raise ValueError('Only one of deg and rad can be supplied') new_marker = MarkerStyle(self) if new_marker._user_transform is None: new_marker._user_transform = Affine2D() From 214ea3f8fc9d6313c857ad6ffe8b13457a1a58d0 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Tue, 28 Sep 2021 10:37:44 +0200 Subject: [PATCH 21/22] Refactor code, linting. --- .../marker_reference.py | 5 +-- .../multivariate_marker_plot.py | 6 ++-- lib/matplotlib/markers.py | 34 +++++++++---------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/examples/lines_bars_and_markers/marker_reference.py b/examples/lines_bars_and_markers/marker_reference.py index ca9d1a77470a..c4564d2b027d 100644 --- a/examples/lines_bars_and_markers/marker_reference.py +++ b/examples/lines_bars_and_markers/marker_reference.py @@ -166,8 +166,9 @@ def split_list(a_list): # Advanced marker modifications with transform # ============================================ # -# Markers can be modified by passing a transform to the MarkerStyle constructor. -# Following example shows how a supplied rotation is applied to several marker shapes. +# Markers can be modified by passing a transform to the MarkerStyle +# constructor. Following example shows how a supplied rotation is applied to +# several marker shapes. common_style = {k: v for k, v in filled_marker_style.items() if k != 'marker'} angles = [0, 10, 20, 30, 45, 60, 90] diff --git a/examples/lines_bars_and_markers/multivariate_marker_plot.py b/examples/lines_bars_and_markers/multivariate_marker_plot.py index 284b21571d59..487bb2b85fd3 100644 --- a/examples/lines_bars_and_markers/multivariate_marker_plot.py +++ b/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -4,9 +4,9 @@ ============================================== This example shows how to use different properties of markers to plot -multivariate datasets. Here we represent a successful baseball throw as a smiley -face with marker size mapped to the skill of thrower, marker rotation to the take-off angle, -and thrust to the marker color. +multivariate datasets. Here we represent a successful baseball throw as a +smiley face with marker size mapped to the skill of thrower, marker rotation to +the take-off angle, and thrust to the marker color. """ import numpy as np diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index a2189ea23d6a..85be6d7651bc 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -82,9 +82,11 @@ plt.plot([1, 2, 3], marker=11) plt.plot([1, 2, 3], marker=matplotlib.markers.CARETDOWNBASE) -Markers join and cap styles can be customized by creating a new instance of MarkerStyle. -A MarkerStyle can also have a custom -`~matplotlib.transforms.Transform` allowing it to be arbitrarily rotated or offset. +Markers join and cap styles can be customized by creating a new instance of +MarkerStyle. +A MarkerStyle can also have a custom `~matplotlib.transforms.Transform` +allowing it to be arbitrarily rotated or offset. + Examples showing the use of markers: * :doc:`/gallery/lines_bars_and_markers/marker_reference` @@ -150,14 +152,6 @@ _empty_path = Path(np.empty((0, 2))) -def _fast_transform_combine(t1, t2): - """Combine two transformations where the second one can be None.""" - if t2 is None: - return t1.frozen() - else: - return (t1 + t2).frozen() - - class MarkerStyle: """ A class representing marker types. @@ -248,8 +242,9 @@ def __init__(self, marker=_unset, fillstyle=None, fillstyle : str, default: :rc:`markers.fillstyle` One of 'full', 'left', 'right', 'bottom', 'top', 'none'. - transform : Affine2D, default: None - Transform that will be combined with the native transform of the marker. + transform : transforms.Transform, default: None + Transform that will be combined with the native transform of the + marker. capstyle : CapStyle, default: None Cap style that will override the default cap style of the marker. @@ -395,7 +390,10 @@ def get_transform(self): Return the transform to be applied to the `.Path` from `MarkerStyle.get_path()`. """ - return _fast_transform_combine(self._transform, self._user_transform) + if self._user_transform is None: + return self._transform.frozen() + else: + return (self._transform + self._user_transform).frozen() def get_alt_path(self): """ @@ -411,8 +409,10 @@ def get_alt_transform(self): Return the transform to be applied to the `.Path` from `MarkerStyle.get_alt_path()`. """ - return _fast_transform_combine(self._alt_transform, - self._user_transform) + if self._user_transform is None: + return self._alt_transform.frozen() + else: + return (self._alt_transform + self._user_transform).frozen() def get_snap_threshold(self): return self._snap_threshold @@ -424,7 +424,7 @@ def get_user_transform(self): def transformed(self, transform: Affine2D): """ - Return a new version of this marker with the transform applied. + Return a new version of this marker with the transform applied. Parameters ---------- From 7101e3c1216c48e65711a97926ccb56027d06c94 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Tue, 28 Sep 2021 15:30:05 +0200 Subject: [PATCH 22/22] Rework get_cap(join)style, add tests Check for user style in get_cap(join)style is removed. This is also supported with two new tests. Additional test checks that get_transform returns combination of supplied and internal transformation. --- lib/matplotlib/markers.py | 4 ++-- lib/matplotlib/tests/test_marker.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 85be6d7651bc..b1d6fa2be6a4 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -322,10 +322,10 @@ def _set_fillstyle(self, fillstyle): self._recache() def get_joinstyle(self): - return self._user_joinstyle or self._joinstyle + return self._joinstyle def get_capstyle(self): - return self._user_capstyle or self._capstyle + return self._capstyle def get_marker(self): return self._marker diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index d3665d9ce09d..894dead24e84 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -207,6 +207,30 @@ def test_marker_clipping(fig_ref, fig_test): ax_test.axis('off') +def test_marker_init_transforms(): + """Test that initializing marker with transform is a simple addition.""" + marker = markers.MarkerStyle("o") + t = Affine2D().translate(1, 1) + t_marker = markers.MarkerStyle("o", transform=t) + assert marker.get_transform() + t == t_marker.get_transform() + + +def test_marker_init_joinstyle(): + marker = markers.MarkerStyle("*") + jstl = markers.JoinStyle.round + styled_marker = markers.MarkerStyle("*", joinstyle=jstl) + assert styled_marker.get_joinstyle() == jstl + assert marker.get_joinstyle() != jstl + + +def test_marker_init_captyle(): + marker = markers.MarkerStyle("*") + capstl = markers.CapStyle.round + styled_marker = markers.MarkerStyle("*", capstyle=capstl) + assert styled_marker.get_capstyle() == capstl + assert marker.get_capstyle() != capstl + + @pytest.mark.parametrize("marker,transform,expected", [ (markers.MarkerStyle("o"), Affine2D().translate(1, 1), Affine2D().translate(1, 1)),