diff --git a/doc/users/next_whats_new/2020-03-16_markerstyle_normalization.rst b/doc/users/next_whats_new/2020-03-16_markerstyle_normalization.rst new file mode 100644 index 000000000000..dce8f34e67bf --- /dev/null +++ b/doc/users/next_whats_new/2020-03-16_markerstyle_normalization.rst @@ -0,0 +1,12 @@ +Allow for custom marker scaling +------------------------------- +`~.markers.MarkerStyle` gained a keyword argument *normalization*, which may be +set to *"none"* to allow for custom paths to not be scaled.:: + + MarkerStyle(Path(...), normalization="none") + +`~.markers.MarkerStyle` also gained a `~.markers.MarkerStyle.set_transform` +method to set affine transformations to existing markers.:: + + m = MarkerStyle("d") + m.set_transform(m.get_transform() + Affine2D().rotate_deg(30)) diff --git a/examples/lines_bars_and_markers/scatter_piecharts.py b/examples/lines_bars_and_markers/scatter_piecharts.py index 6b2b4aa88824..b24f5fd2af8a 100644 --- a/examples/lines_bars_and_markers/scatter_piecharts.py +++ b/examples/lines_bars_and_markers/scatter_piecharts.py @@ -3,15 +3,19 @@ Scatter plot with pie chart markers =================================== -This example makes custom 'pie charts' as the markers for a scatter plot. - -Thanks to Manuel Metz for the example. +This example shows two methods to make custom 'pie charts' as the markers +for a scatter plot. """ +########################################################################## +# Manually creating marker vertices +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# + import numpy as np import matplotlib.pyplot as plt -# first define the ratios +# first define the cumulative ratios r1 = 0.2 # 20% r2 = r1 + 0.4 # 40% @@ -36,10 +40,55 @@ s3 = np.abs(xy3).max() fig, ax = plt.subplots() -ax.scatter(range(3), range(3), marker=xy1, s=s1**2 * sizes, facecolor='blue') -ax.scatter(range(3), range(3), marker=xy2, s=s2**2 * sizes, facecolor='green') -ax.scatter(range(3), range(3), marker=xy3, s=s3**2 * sizes, facecolor='red') +ax.scatter(range(3), range(3), marker=xy1, s=s1**2 * sizes, facecolor='C0') +ax.scatter(range(3), range(3), marker=xy2, s=s2**2 * sizes, facecolor='C1') +ax.scatter(range(3), range(3), marker=xy3, s=s3**2 * sizes, facecolor='C2') + +plt.show() + + +########################################################################## +# Using wedges as markers +# ~~~~~~~~~~~~~~~~~~~~~~~ +# +# An alternative is to create custom markers from the `~.path.Path` of a +# `~.patches.Wedge`, which might be more versatile. +# + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Wedge +from matplotlib.markers import MarkerStyle + +# first define the ratios +r1 = 0.2 # 20% +r2 = r1 + 0.3 # 50% +r3 = 1 - r1 - r2 # 30% + + +def markers_from_ratios(ratios, width=1): + markers = [] + angles = 360*np.concatenate(([0], np.cumsum(ratios))) + for i in range(len(angles)-1): + # create a Wedge within the unit square in between the given angles... + w = Wedge((0, 0), 0.5, angles[i], angles[i+1], width=width/2) + # ... and create a custom Marker from its path. + markers.append(MarkerStyle(w.get_path(), normalization="none")) + return markers + +# define some sizes of the scatter marker +sizes = np.array([100, 200, 400, 800]) +# collect the markers and some colors +markers = markers_from_ratios([r1, r2, r3], width=0.6) +colors = plt.cm.tab10.colors[:len(markers)] + +fig, ax = plt.subplots() + +for marker, color in zip(markers, colors): + ax.scatter(range(len(sizes)), range(len(sizes)), marker=marker, s=sizes, + edgecolor="none", facecolor=color) +ax.margins(0.1) plt.show() ############################################################################# @@ -55,3 +104,5 @@ import matplotlib matplotlib.axes.Axes.scatter matplotlib.pyplot.scatter +matplotlib.patches.Wedge +matplotlib.markers.MarkerStyle diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index ccbdce01116b..3d36bb0c947c 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -201,7 +201,8 @@ class MarkerStyle: # TODO: Is this ever used as a non-constant? _point_size_reduction = 0.5 - def __init__(self, marker=None, fillstyle=None): + def __init__(self, marker=None, fillstyle=None, *, + normalization="classic"): """ Attributes ---------- @@ -213,12 +214,23 @@ def __init__(self, marker=None, fillstyle=None): Parameters ---------- - marker : str or array-like, optional, default: None + marker : str, array-like, `~.path.Path`, or `~.markers.MarkerStyle`, \ + default: None See the descriptions of possible markers in the module docstring. fillstyle : str, optional, default: 'full' 'full', 'left", 'right', 'bottom', 'top', 'none' + + normalization : str, {'classic', 'none'}, optional, default: "classic" + The normalization of the marker size. Only applies to custom paths + that are provided as array of vertices or `~.path.Path`. + Can take two values: + *'classic'*, being the default, makes sure the marker path is + normalized to fit within a unit-square by affine scaling. + *'none'*, in which case no scaling is performed on the marker path. """ + cbook._check_in_list(["classic", "none"], normalization=normalization) + self._normalize = normalization self._marker_function = None self.set_fillstyle(fillstyle) self.set_marker(marker) @@ -303,6 +315,13 @@ def get_path(self): def get_transform(self): return self._transform.frozen() + def set_transform(self, transform): + """ + Sets the transform of the marker. This is the transform by which the + marker path is transformed. + """ + self._transform = transform + def get_alt_path(self): return self._alt_path @@ -316,8 +335,9 @@ def _set_nothing(self): self._filled = False def _set_custom_marker(self, path): - rescale = np.max(np.abs(path.vertices)) # max of x's and y's. - self._transform = Affine2D().scale(0.5 / rescale) + if self._normalize == "classic": + rescale = np.max(np.abs(path.vertices)) # max of x's and y's. + self._transform = Affine2D().scale(0.5 / rescale) self._path = path def _set_path_marker(self): @@ -350,8 +370,6 @@ def _set_tuple_marker(self): def _set_mathtext_path(self): """ Draws mathtext markers '$...$' using TextPath object. - - Submitted by tcb """ from matplotlib.text import TextPath diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index e50746165792..f34c27896701 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -2,8 +2,9 @@ import matplotlib.pyplot as plt from matplotlib import markers from matplotlib.path import Path -from matplotlib.testing.decorators import check_figures_equal +from matplotlib.transforms import Affine2D +from matplotlib.testing.decorators import check_figures_equal import pytest @@ -133,3 +134,24 @@ def draw_ref_marker(y, style, size): ax_test.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5)) ax_ref.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5)) + + +@check_figures_equal(extensions=["png"]) +def test_marker_normalization(fig_test, fig_ref): + plt.style.use("mpl20") + + ax = fig_ref.subplots() + ax.margins(0.3) + ax.scatter([0, 1], [0, 0], s=400, marker="s", c="C2") + + ax = fig_test.subplots() + ax.margins(0.3) + # test normalize + p = Path([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], closed=True) + p1 = p.transformed(Affine2D().translate(-.5, -.5).scale(20)) + m1 = markers.MarkerStyle(p1, normalization="none") + ax.scatter([0], [0], s=1, marker=m1, c="C2") + # test transform + m2 = markers.MarkerStyle("s") + m2.set_transform(m2.get_transform() + Affine2D().scale(20)) + ax.scatter([1], [0], s=1, marker=m2, c="C2")