Skip to content

Commit c4628b7

Browse files
marker-transforms
1 parent 4b5fc44 commit c4628b7

File tree

4 files changed

+117
-14
lines changed

4 files changed

+117
-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

+24-6
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,12 +214,23 @@ 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+
cbook._check_in_list(["classic", "none"], normalization=normalization)
233+
self._normalize = normalization
222234
self._marker_function = None
223235
self.set_fillstyle(fillstyle)
224236
self.set_marker(marker)
@@ -303,6 +315,13 @@ def get_path(self):
303315
def get_transform(self):
304316
return self._transform.frozen()
305317

318+
def set_transform(self, transform):
319+
"""
320+
Sets the transform of the marker. This is the transform by which the
321+
marker path is transformed.
322+
"""
323+
self._transform = transform
324+
306325
def get_alt_path(self):
307326
return self._alt_path
308327

@@ -316,8 +335,9 @@ def _set_nothing(self):
316335
self._filled = False
317336

318337
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)
338+
if self._normalize == "classic":
339+
rescale = np.max(np.abs(path.vertices)) # max of x's and y's.
340+
self._transform = Affine2D().scale(0.5 / rescale)
321341
self._path = path
322342

323343
def _set_path_marker(self):
@@ -350,8 +370,6 @@ def _set_tuple_marker(self):
350370
def _set_mathtext_path(self):
351371
"""
352372
Draws mathtext markers '$...$' using TextPath object.
353-
354-
Submitted by tcb
355373
"""
356374
from matplotlib.text import TextPath
357375

lib/matplotlib/tests/test_marker.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import matplotlib.pyplot as plt
33
from matplotlib import markers
44
from matplotlib.path import Path
5-
from matplotlib.testing.decorators import check_figures_equal
5+
from matplotlib.transforms import Affine2D
66

7+
from matplotlib.testing.decorators import check_figures_equal
78
import pytest
89

910

@@ -133,3 +134,24 @@ def draw_ref_marker(y, style, size):
133134

134135
ax_test.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5))
135136
ax_ref.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5))
137+
138+
139+
@check_figures_equal(extensions=["png"])
140+
def test_marker_normalization(fig_test, fig_ref):
141+
plt.style.use("mpl20")
142+
143+
ax = fig_ref.subplots()
144+
ax.margins(0.3)
145+
ax.scatter([0, 1], [0, 0], s=400, marker="s", c="C2")
146+
147+
ax = fig_test.subplots()
148+
ax.margins(0.3)
149+
# test normalize
150+
p = Path([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], closed=True)
151+
p1 = p.transformed(Affine2D().translate(-.5, -.5).scale(20))
152+
m1 = markers.MarkerStyle(p1, normalization="none")
153+
ax.scatter([0], [0], s=1, marker=m1, c="C2")
154+
# test transform
155+
m2 = markers.MarkerStyle("s")
156+
m2.set_transform(m2.get_transform() + Affine2D().scale(20))
157+
ax.scatter([1], [0], s=1, marker=m2, c="C2")

0 commit comments

Comments
 (0)