From fdf0a688c775b779281757892210e6207b03f455 Mon Sep 17 00:00:00 2001 From: Manuel GOACOLOU Date: Tue, 25 Feb 2014 18:11:26 +0100 Subject: [PATCH 1/7] Rotate markers in Scatter plot --- doc/users/whats_new.rst | 7 ++++ .../pylab_examples/scatter_rotate_symbol.py | 15 +++++++ lib/matplotlib/axes/_axes.py | 9 ++++- lib/matplotlib/collections.py | 39 +++++++++++++++++-- 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 examples/pylab_examples/scatter_rotate_symbol.py diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index ab6ab4f39b71..f4604b0ebbb6 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -801,6 +801,13 @@ values to the interval [0,1] with power-law scaling with the exponent provided by the constructor's `gamma` argument. Power law normalization can be useful for, e.g., emphasizing small populations in a histogram. +Add Rotate marker capabilities in scatter plot +`````````````````````````````````````````````` + +mgoacolou add `angles` parameter to :func:`~matplotlib.pyplot.scatter`. +:ref:`~examples/pylab_examples/scatter_rotate_symbol.py` + + Fully customizable boxplots ``````````````````````````` Paul Hobson overhauled the :func:`~matplotlib.pyplot.boxplot` method such diff --git a/examples/pylab_examples/scatter_rotate_symbol.py b/examples/pylab_examples/scatter_rotate_symbol.py new file mode 100644 index 000000000000..2365e87ab44c --- /dev/null +++ b/examples/pylab_examples/scatter_rotate_symbol.py @@ -0,0 +1,15 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.markers import TICKRIGHT + +rx, ry = 3., 1. +area = rx * ry * np.pi +angles = np.linspace(0., 360., 30.) + +x, y, sizes, colors = np.random.rand(4, 30) +sizes *= 20**2. + +plt.scatter(x, y, sizes, colors, marker="*", angles=angles, zorder=2) +plt.scatter(x, y, 2.5*sizes, colors, marker=TICKRIGHT, angles=angles, zorder=2) + +plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index e695b32baa1a..c501a59edf75 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3758,6 +3758,9 @@ def scatter(self, x, y, s=None, c=None, marker='o', cmap=None, norm=None, an instance of the class or the text shorthand for a particular marker. + angles : scalar or array_like, shape (n, ), optional, default: 0 + degrees counter clock-wise from X axis + cmap : `~matplotlib.colors.Colormap`, optional, default: None A `~matplotlib.colors.Colormap` instance or registered name. `cmap` is only used if `c` is an array of floats. If None, @@ -3861,13 +3864,15 @@ def scatter(self, x, y, s=None, c=None, marker='o', cmap=None, norm=None, if x.size != y.size: raise ValueError("x and y must be the same size") + angles = np.ma.ravel(angles) # This doesn't have to match x, y in size. + if s is None: if rcParams['_internal.classic_mode']: s = 20 else: s = rcParams['lines.markersize'] ** 2.0 - s = np.ma.ravel(s) # This doesn't have to match x, y in size. + x, y, s, c, angles = cbook.delete_masked_points(x, y, s, c, angles) # After this block, c_array will be None unless # c is an array for mapping. The potential ambiguity @@ -3913,7 +3918,7 @@ def scatter(self, x, y, s=None, c=None, marker='o', cmap=None, norm=None, offsets = np.dstack((x, y)) collection = mcoll.PathCollection( - (path,), scales, + (path,), scales, angles, facecolors=colors, edgecolors=edgecolors, linewidths=linewidths, diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 5576def9d440..be5a016d760d 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -820,13 +820,40 @@ def draw(self, renderer): self.set_sizes(self._sizes, self.figure.dpi) Collection.draw(self, renderer) +class _CollectionWithRotations(Collection): + """ + Base class for collections that have an array of rotations. + """ + def get_rotations(self): + return self._rotations + + def set_rotations(self, rotations): + if rotations is None: + self._rotations = np.array([]) + self._transforms = np.empty((0, 3, 3)) + else: + self._rotations = np.asarray(rotations) + self._transforms = np.zeros((len(self._rotations), 3, 3)) + rot = np.deg2rad(self._rotations) + rot_c = np.cos(rot) + rot_s = np.sin(rot) + self._transforms[:, 0, 0] = rot_c + self._transforms[:, 0, 1] = -rot_s + self._transforms[:, 1, 1] = rot_c + self._transforms[:, 1, 0] = rot_s + self._transforms[:, 2, 2] = 1.0 -class PathCollection(_CollectionWithSizes): + def draw(self, renderer): + self.set_rotations(self._rotations) + Collection.draw(self, renderer) + + +class PathCollection(_CollectionWithSizes, _CollectionWithRotations): """ This is the most basic :class:`Collection` subclass. """ @docstring.dedent_interpd - def __init__(self, paths, sizes=None, **kwargs): + def __init__(self, paths, sizes=None, angles=None, **kwargs): """ *paths* is a sequence of :class:`matplotlib.path.Path` instances. @@ -838,15 +865,21 @@ def __init__(self, paths, sizes=None, **kwargs): self.set_paths(paths) self.set_sizes(sizes) self.stale = True + self.set_rotations(angles) def set_paths(self, paths): + """ + update the paths sequence + """ self._paths = paths self.stale = True def get_paths(self): + """ + return the paths sequence + """ return self._paths - class PolyCollection(_CollectionWithSizes): @docstring.dedent_interpd def __init__(self, verts, sizes=None, closed=True, **kwargs): From ead934df14094088aaddcd62ad6e0c3a3f1d3e40 Mon Sep 17 00:00:00 2001 From: Manuel GOACOLOU Date: Tue, 25 Feb 2014 18:15:44 +0100 Subject: [PATCH 2/7] Rename _CollectionWithRotations to _CollectionWithAngles apply incrementaly the Affine2D transform for size and angle use super for draw method --- .../pylab_examples/scatter_rotate_symbol.py | 8 ++- lib/matplotlib/collections.py | 70 +++++++++++++------ 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/examples/pylab_examples/scatter_rotate_symbol.py b/examples/pylab_examples/scatter_rotate_symbol.py index 2365e87ab44c..f8c32d3c54e9 100644 --- a/examples/pylab_examples/scatter_rotate_symbol.py +++ b/examples/pylab_examples/scatter_rotate_symbol.py @@ -2,14 +2,16 @@ import matplotlib.pyplot as plt from matplotlib.markers import TICKRIGHT +nbpts = 50 rx, ry = 3., 1. area = rx * ry * np.pi -angles = np.linspace(0., 360., 30.) +angles = np.linspace(0., 360., nbpts) -x, y, sizes, colors = np.random.rand(4, 30) -sizes *= 20**2. +x, y, sizes, colors = np.random.rand(4, nbpts) +sizes *= 20. plt.scatter(x, y, sizes, colors, marker="*", angles=angles, zorder=2) plt.scatter(x, y, 2.5*sizes, colors, marker=TICKRIGHT, angles=angles, zorder=2) plt.show() + diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index be5a016d760d..8d41044a26ef 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -808,47 +808,68 @@ def set_sizes(self, sizes, dpi=72.0): self._transforms = np.empty((0, 3, 3)) else: self._sizes = np.asarray(sizes) - self._transforms = np.zeros((len(self._sizes), 3, 3)) scale = np.sqrt(self._sizes) * dpi / 72.0 * self._factor - self._transforms[:, 0, 0] = scale - self._transforms[:, 1, 1] = scale - self._transforms[:, 2, 2] = 1.0 + if self._transforms is None or \ + (isinstance(self._transforms, list) and not self._transforms) or \ + (isinstance(self._transforms, np.ndarray) and not self._transforms.any()): + self._transforms = np.zeros((len(self._sizes), 3, 3)) + self._transforms[:, 0, 0] = scale + self._transforms[:, 1, 1] = scale + self._transforms[:, 2, 2] = 1.0 self.stale = True + s = np.zeros((len(self._sizes),3,3)) + s[:, 0, 0] = scale + s[:, 1, 1] = scale + s[:, 2, 2] = 1.0 + for i in xrange(self._transforms.shape[0]): + self._transforms[i,:,:] = self._transforms[i,:,:].dot(s[i%len(self._sizes),:,:]) @allow_rasterization def draw(self, renderer): self.set_sizes(self._sizes, self.figure.dpi) - Collection.draw(self, renderer) + super(_CollectionWithSizes, self).draw(renderer) -class _CollectionWithRotations(Collection): +class _CollectionWithAngles(Collection): """ - Base class for collections that have an array of rotations. + Base class for collections that have an array of angles. """ - def get_rotations(self): - return self._rotations + def get_angles(self): + return self._angles - def set_rotations(self, rotations): - if rotations is None: - self._rotations = np.array([]) + def set_angles(self, angles): + if angles is None: + self._angles = np.array([]) self._transforms = np.empty((0, 3, 3)) else: - self._rotations = np.asarray(rotations) - self._transforms = np.zeros((len(self._rotations), 3, 3)) - rot = np.deg2rad(self._rotations) + self._angles = np.asarray(angles) + rot = np.deg2rad(90.-self._angles) rot_c = np.cos(rot) rot_s = np.sin(rot) - self._transforms[:, 0, 0] = rot_c - self._transforms[:, 0, 1] = -rot_s - self._transforms[:, 1, 1] = rot_c - self._transforms[:, 1, 0] = rot_s - self._transforms[:, 2, 2] = 1.0 + if self._transforms is None or \ + (isinstance(self._transforms, list) and not self._transforms) or \ + (isinstance(self._transforms, np.ndarray) and not self._transforms.any()): + self._transforms = np.zeros((len(self._angles), 3, 3)) + self._transforms[:, 0, 0] = rot_c + self._transforms[:, 0, 1] = -rot_s + self._transforms[:, 1, 1] = rot_c + self._transforms[:, 1, 0] = rot_s + self._transforms[:, 2, 2] = 1.0 + else: + r = np.zeros((len(self._angles), 3, 3)) + r[:, 0, 0] = rot_c + r[:, 0, 1] = -rot_s + r[:, 1, 1] = rot_c + r[:, 1, 0] = rot_s + r[:, 2, 2] = 1.0 + for i in xrange(self._transforms.shape[0]): + self._transforms[i,:,:] = self._transforms[i,:,:].dot(r[i%len(self._angles),:,:]) def draw(self, renderer): - self.set_rotations(self._rotations) - Collection.draw(self, renderer) + self.set_angles(self._angles) + super(_CollectionWithAngles, self).draw(renderer) -class PathCollection(_CollectionWithSizes, _CollectionWithRotations): +class PathCollection(_CollectionWithSizes, _CollectionWithAngles): """ This is the most basic :class:`Collection` subclass. """ @@ -880,6 +901,9 @@ def get_paths(self): """ return self._paths + def draw(self, renderer): + super(PathCollection, self).draw(renderer) + class PolyCollection(_CollectionWithSizes): @docstring.dedent_interpd def __init__(self, verts, sizes=None, closed=True, **kwargs): From f2107c2868f0d6bbbacbd130c62c8ef069083010 Mon Sep 17 00:00:00 2001 From: Manuel GOACOLOU Date: Tue, 25 Feb 2014 18:18:19 +0100 Subject: [PATCH 3/7] apply only once the scale and rotation --- examples/pylab_examples/scatter_rotate_symbol.py | 2 +- lib/matplotlib/collections.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/pylab_examples/scatter_rotate_symbol.py b/examples/pylab_examples/scatter_rotate_symbol.py index f8c32d3c54e9..e0aab8dd4455 100644 --- a/examples/pylab_examples/scatter_rotate_symbol.py +++ b/examples/pylab_examples/scatter_rotate_symbol.py @@ -8,7 +8,7 @@ angles = np.linspace(0., 360., nbpts) x, y, sizes, colors = np.random.rand(4, nbpts) -sizes *= 20. +sizes *= 2000. plt.scatter(x, y, sizes, colors, marker="*", angles=angles, zorder=2) plt.scatter(x, y, 2.5*sizes, colors, marker=TICKRIGHT, angles=angles, zorder=2) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 8d41044a26ef..1f2c5c9ad60d 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -822,11 +822,10 @@ def set_sizes(self, sizes, dpi=72.0): s[:, 1, 1] = scale s[:, 2, 2] = 1.0 for i in xrange(self._transforms.shape[0]): - self._transforms[i,:,:] = self._transforms[i,:,:].dot(s[i%len(self._sizes),:,:]) + self._transforms[i,:,:] = np.dot(s[i%len(self._sizes),:,:], self._transforms[i,:,:]) @allow_rasterization def draw(self, renderer): - self.set_sizes(self._sizes, self.figure.dpi) super(_CollectionWithSizes, self).draw(renderer) class _CollectionWithAngles(Collection): @@ -842,7 +841,7 @@ def set_angles(self, angles): self._transforms = np.empty((0, 3, 3)) else: self._angles = np.asarray(angles) - rot = np.deg2rad(90.-self._angles) + rot = np.deg2rad(self._angles) rot_c = np.cos(rot) rot_s = np.sin(rot) if self._transforms is None or \ @@ -862,10 +861,9 @@ def set_angles(self, angles): r[:, 1, 0] = rot_s r[:, 2, 2] = 1.0 for i in xrange(self._transforms.shape[0]): - self._transforms[i,:,:] = self._transforms[i,:,:].dot(r[i%len(self._angles),:,:]) + self._transforms[i,:,:] = np.dot(r[i%len(self._angles),:,:], self._transforms[i,:,:]) def draw(self, renderer): - self.set_angles(self._angles) super(_CollectionWithAngles, self).draw(renderer) From dec2f182002daef07fbbd7ceec3b5fc886814e29 Mon Sep 17 00:00:00 2001 From: Manuel GOACOLOU Date: Tue, 25 Feb 2014 18:20:37 +0100 Subject: [PATCH 4/7] correct the angle orientation to clock-wize --- lib/matplotlib/collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 1f2c5c9ad60d..6a23eaf6d547 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -841,7 +841,7 @@ def set_angles(self, angles): self._transforms = np.empty((0, 3, 3)) else: self._angles = np.asarray(angles) - rot = np.deg2rad(self._angles) + rot = np.deg2rad(-self._angles) rot_c = np.cos(rot) rot_s = np.sin(rot) if self._transforms is None or \ From 5871024df2114d2f723cfcc7ea5b3fbbdd796e45 Mon Sep 17 00:00:00 2001 From: Manuel GOACOLOU Date: Wed, 26 Feb 2014 10:04:15 +0100 Subject: [PATCH 5/7] unset affine transform for size and angle before applying the new parameters --- lib/matplotlib/collections.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 6a23eaf6d547..e27b1c10dd59 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -790,6 +790,18 @@ def get_sizes(self): """ return self._sizes + def __unset_sizes(self): + if len(self._sizes) == 0: + return + scale = np.sqrt(self._sizes) * self.__dpi / 72.0 + s = np.zeros((len(self._sizes),3,3)) + s[:, 0, 0] = scale + s[:, 1, 1] = scale + s[:, 2, 2] = 1.0 + for i in xrange(self._transforms.shape[0]): + isc = np.linalg.inv(s[i%len(self._sizes),:,:]) + self._transforms[i,:,:] = np.dot(isc, self._transforms[i,:,:]) + def set_sizes(self, sizes, dpi=72.0): """ Set the sizes of each member of the collection. @@ -835,7 +847,24 @@ class _CollectionWithAngles(Collection): def get_angles(self): return self._angles + def __unset_angles(self): + if len(self._angles) == 0: + return + rot = np.deg2rad(-self._angles) + rot_c = np.cos(rot) + rot_s = np.sin(rot) + r = np.zeros((len(self._angles), 3, 3)) + r[:, 0, 0] = rot_c + r[:, 0, 1] = -rot_s + r[:, 1, 1] = rot_c + r[:, 1, 0] = rot_s + r[:, 2, 2] = 1.0 + for i in xrange(self._transforms.shape[0]): + irt = np.linalg.inv(r[i%len(self._angles),:,:]) + self._transforms[i,:,:] = np.dot(irt, self._transforms[i,:,:]) + def set_angles(self, angles): + self.__unset_angles() if angles is None: self._angles = np.array([]) self._transforms = np.empty((0, 3, 3)) From c8574a27d797cd5e1ebe3ab3915843eda8f231a9 Mon Sep 17 00:00:00 2001 From: Manuel GOACOLOU Date: Wed, 26 Feb 2014 10:52:06 +0100 Subject: [PATCH 6/7] resize transforms to feat at least the sizes/angles length --- lib/matplotlib/collections.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index e27b1c10dd59..7c16b6183f4f 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -778,6 +778,9 @@ class _CollectionWithSizes(Collection): """ _factor = 1.0 + def __init__(self): + self._sizes = np.array([]) + def get_sizes(self): """ Returns the sizes of the elements in the collection. The @@ -833,6 +836,9 @@ def set_sizes(self, sizes, dpi=72.0): s[:, 0, 0] = scale s[:, 1, 1] = scale s[:, 2, 2] = 1.0 + if self._transforms.shape[0] < len(self._sizes): + # resize transforms to feat at least the sizes length + self._transforms = np.resize(self._transforms,(len(self._sizes),3,3)) for i in xrange(self._transforms.shape[0]): self._transforms[i,:,:] = np.dot(s[i%len(self._sizes),:,:], self._transforms[i,:,:]) @@ -844,6 +850,9 @@ class _CollectionWithAngles(Collection): """ Base class for collections that have an array of angles. """ + def __init__(self): + self._angles = np.array([]) + def get_angles(self): return self._angles @@ -889,6 +898,9 @@ def set_angles(self, angles): r[:, 1, 1] = rot_c r[:, 1, 0] = rot_s r[:, 2, 2] = 1.0 + if self._transforms.shape[0] < len(self._angles): + # resize transforms to feat at least the angles length + self._transforms = np.resize(self._transforms,(len(self._angles),3,3)) for i in xrange(self._transforms.shape[0]): self._transforms[i,:,:] = np.dot(r[i%len(self._angles),:,:], self._transforms[i,:,:]) From 57ac137cc980aeafcf5c9c899266d6444a873002 Mon Sep 17 00:00:00 2001 From: Manuel GOACOLOU Date: Wed, 26 Feb 2014 23:16:41 +0100 Subject: [PATCH 7/7] correct the kw passing to Collection from PathCollection --- lib/matplotlib/collections.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 7c16b6183f4f..c2a86e96099b 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -778,7 +778,8 @@ class _CollectionWithSizes(Collection): """ _factor = 1.0 - def __init__(self): + def __init__(self, *arg, **kw): + super(_CollectionWithSizes, self).__init__(*arg, **kw) self._sizes = np.array([]) def get_sizes(self): @@ -850,7 +851,8 @@ class _CollectionWithAngles(Collection): """ Base class for collections that have an array of angles. """ - def __init__(self): + def __init__(self, *arg, **kw): + super(_CollectionWithAngles, self).__init__(*arg, **kw) self._angles = np.array([]) def get_angles(self): @@ -920,8 +922,7 @@ def __init__(self, paths, sizes=None, angles=None, **kwargs): %(Collection)s """ - - Collection.__init__(self, **kwargs) + super(PathCollection, self).__init__(**kwargs) self.set_paths(paths) self.set_sizes(sizes) self.stale = True