Skip to content

Commit 92192aa

Browse files
marker-transforms
1 parent b42d6d9 commit 92192aa

File tree

4 files changed

+121
-14
lines changed

4 files changed

+121
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Allow for custom marker scaling
2+
-------------------------------
3+
`~.markers.MarkerStyle` gained a keyword argument *normalization*, which may be
4+
set to *"none"* to allow for custom paths to not be scaled.::
5+
6+
MarkerStyle(Path(...), normalization="none")
7+
8+
`~.markers.MarkerStyle` also gained a `~.markers.MarkerStyle`.set_transform`
9+
method to set affine transformations to existing markers.::
10+
11+
m = MarkerStyle("d")
12+
m.set_transform(m.get_transform() + Affine2D().rotate_deg(30))

examples/lines_bars_and_markers/scatter_piecharts.py

+58-7
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
Scatter plot with pie chart markers
44
===================================
55
6-
This example makes custom 'pie charts' as the markers for a scatter plot.
7-
8-
Thanks to Manuel Metz for the example.
6+
This example shows two methods to make custom 'pie charts' as the markers
7+
for a scatter plot.
98
"""
109

10+
##########################################################################
11+
# Manually creating marker vertices
12+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13+
#
14+
1115
import numpy as np
1216
import matplotlib.pyplot as plt
1317

14-
# first define the ratios
18+
# first define the cumulative ratios
1519
r1 = 0.2 # 20%
1620
r2 = r1 + 0.4 # 40%
1721

@@ -36,10 +40,55 @@
3640
s3 = np.abs(xy3).max()
3741

3842
fig, ax = plt.subplots()
39-
ax.scatter(range(3), range(3), marker=xy1, s=s1**2 * sizes, facecolor='blue')
40-
ax.scatter(range(3), range(3), marker=xy2, s=s2**2 * sizes, facecolor='green')
41-
ax.scatter(range(3), range(3), marker=xy3, s=s3**2 * sizes, facecolor='red')
43+
ax.scatter(range(3), range(3), marker=xy1, s=s1**2 * sizes, facecolor='C0')
44+
ax.scatter(range(3), range(3), marker=xy2, s=s2**2 * sizes, facecolor='C1')
45+
ax.scatter(range(3), range(3), marker=xy3, s=s3**2 * sizes, facecolor='C2')
46+
47+
plt.show()
48+
49+
50+
##########################################################################
51+
# Using wedges as markers
52+
# ~~~~~~~~~~~~~~~~~~~~~~~
53+
#
54+
# An alternative is to create custom markers from the `~.path.Path` of a
55+
# `~.patches.Wedge`, which might be more versatile.
56+
#
57+
58+
import numpy as np
59+
import matplotlib.pyplot as plt
60+
from matplotlib.patches import Wedge
61+
from matplotlib.markers import MarkerStyle
62+
63+
# first define the ratios
64+
r1 = 0.2 # 20%
65+
r2 = r1 + 0.3 # 50%
66+
r3 = 1 - r1 - r2 # 30%
67+
68+
69+
def markers_from_ratios(ratios, width=1):
70+
markers = []
71+
angles = 360*np.concatenate(([0], np.cumsum(ratios)))
72+
for i in range(len(angles)-1):
73+
# create a Wedge within the unit square in between the given angles...
74+
w = Wedge((0, 0), 0.5, angles[i], angles[i+1], width=width/2)
75+
# ... and create a custom Marker from its path.
76+
markers.append(MarkerStyle(w.get_path(), normalization="none"))
77+
return markers
78+
79+
# define some sizes of the scatter marker
80+
sizes = np.array([100, 200, 400, 800])
81+
# collect the markers and some colors
82+
markers = markers_from_ratios([r1, r2, r3], width=0.6)
83+
colors = plt.cm.tab10.colors[:len(markers)]
84+
85+
fig, ax = plt.subplots()
86+
87+
for marker, color in zip(markers, colors):
88+
ax.scatter(range(len(sizes)), range(len(sizes)), marker=marker, s=sizes,
89+
edgecolor="none", facecolor=color)
4290

91+
ax.margins(0.1)
4392
plt.show()
4493

4594
#############################################################################
@@ -55,3 +104,5 @@
55104
import matplotlib
56105
matplotlib.axes.Axes.scatter
57106
matplotlib.pyplot.scatter
107+
matplotlib.patches.Wedge
108+
matplotlib.markers.MarkerStyle

lib/matplotlib/markers.py

+27-7
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ class MarkerStyle:
201201
# TODO: Is this ever used as a non-constant?
202202
_point_size_reduction = 0.5
203203

204-
def __init__(self, marker=None, fillstyle=None):
204+
def __init__(self, marker=None, fillstyle=None, *,
205+
normalization="classic"):
205206
"""
206207
Attributes
207208
----------
@@ -213,18 +214,31 @@ def __init__(self, marker=None, fillstyle=None):
213214
214215
Parameters
215216
----------
216-
marker : str or array-like, optional, default: None
217+
marker : str, array-like, `~.path.Path`, or `~.markers.MarkerStyle`, \
218+
default: None
217219
See the descriptions of possible markers in the module docstring.
218220
219221
fillstyle : str, optional, default: 'full'
220222
'full', 'left", 'right', 'bottom', 'top', 'none'
223+
224+
normalization : str, {'classic', 'none'}, optional, default: "classic"
225+
The normalization of the marker size. Only applies to custom paths
226+
that are provided as array of vertices or `~.path.Path`.
227+
Can take two values:
228+
*'classic'*, being the default, makes sure the marker path is
229+
normalized to fit within a unit-square by affine scaling.
230+
*'none'*, in which case no scaling is performed on the marker path.
221231
"""
232+
if normalization not in ["classic", "none"]:
233+
raise ValueError("normalization={!r} is not valid; it must be "
234+
"'classic' or 'none'".format(normalization))
235+
self._normalize = normalization
222236
self._marker_function = None
223237
self.set_fillstyle(fillstyle)
224238
self.set_marker(marker)
225239

226240
def _recache(self):
227-
if self._marker_function is None:
241+
if self._marker_function is None or self._normalize == "none":
228242
return
229243
self._path = _empty_path
230244
self._transform = IdentityTransform()
@@ -303,6 +317,13 @@ def get_path(self):
303317
def get_transform(self):
304318
return self._transform.frozen()
305319

320+
def set_transform(self, transform):
321+
"""
322+
Sets the transform of the marker. This is the transform by which the
323+
marker path is transformed.
324+
"""
325+
self._transform = transform
326+
306327
def get_alt_path(self):
307328
return self._alt_path
308329

@@ -316,8 +337,9 @@ def _set_nothing(self):
316337
self._filled = False
317338

318339
def _set_custom_marker(self, path):
319-
rescale = np.max(np.abs(path.vertices)) # max of x's and y's.
320-
self._transform = Affine2D().scale(0.5 / rescale)
340+
if self._normalize == "classic":
341+
rescale = np.max(np.abs(path.vertices)) # max of x's and y's.
342+
self._transform = Affine2D().scale(0.5 / rescale)
321343
self._path = path
322344

323345
def _set_path_marker(self):
@@ -349,8 +371,6 @@ def _set_tuple_marker(self):
349371
def _set_mathtext_path(self):
350372
"""
351373
Draws mathtext markers '$...$' using TextPath object.
352-
353-
Submitted by tcb
354374
"""
355375
from matplotlib.text import TextPath
356376

lib/matplotlib/tests/test_marker.py

+24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import numpy as np
22
from matplotlib import markers
33
from matplotlib.path import Path
4+
from matplotlib.transforms import Affine2D
5+
import matplotlib.pyplot as plt
46

7+
from matplotlib.testing.decorators import check_figures_equal
58
import pytest
69

710

@@ -26,3 +29,24 @@ def test_marker_path():
2629
path = Path([[0, 0], [1, 0]], [Path.MOVETO, Path.LINETO])
2730
# Checking this doesn't fail.
2831
marker_style.set_marker(path)
32+
33+
34+
@check_figures_equal(extensions=["png"])
35+
def test_marker_normalization(fig_test, fig_ref):
36+
plt.style.use("mpl20")
37+
38+
ax = fig_ref.subplots()
39+
ax.margins(0.3)
40+
ax.scatter([0, 1], [0, 0], s=400, marker="s", c="C2")
41+
42+
ax = fig_test.subplots()
43+
ax.margins(0.3)
44+
# test normalize
45+
p = Path([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], closed=True)
46+
p1 = p.transformed(Affine2D().translate(-.5, -.5).scale(20))
47+
m1 = markers.MarkerStyle(p1, normalization="none")
48+
ax.scatter([0], [0], s=1, marker=m1, c="C2")
49+
# test transform
50+
m2 = markers.MarkerStyle("s")
51+
m2.set_transform(m2.get_transform() + Affine2D().scale(20))
52+
ax.scatter([1], [0], s=1, marker=m2, c="C2")

0 commit comments

Comments
 (0)