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..e0aab8dd4455 --- /dev/null +++ b/examples/pylab_examples/scatter_rotate_symbol.py @@ -0,0 +1,17 @@ +import numpy as np +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., nbpts) + +x, y, sizes, colors = np.random.rand(4, nbpts) +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) + +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..c2a86e96099b 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -778,6 +778,10 @@ class _CollectionWithSizes(Collection): """ _factor = 1.0 + def __init__(self, *arg, **kw): + super(_CollectionWithSizes, self).__init__(*arg, **kw) + self._sizes = np.array([]) + def get_sizes(self): """ Returns the sizes of the elements in the collection. The @@ -790,6 +794,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. @@ -808,44 +824,125 @@ 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 + 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,:,:]) @allow_rasterization def draw(self, renderer): - self.set_sizes(self._sizes, self.figure.dpi) - Collection.draw(self, renderer) + super(_CollectionWithSizes, self).draw(renderer) + +class _CollectionWithAngles(Collection): + """ + Base class for collections that have an array of angles. + """ + def __init__(self, *arg, **kw): + super(_CollectionWithAngles, self).__init__(*arg, **kw) + self._angles = np.array([]) + 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)) + else: + self._angles = np.asarray(angles) + rot = np.deg2rad(-self._angles) + rot_c = np.cos(rot) + rot_s = np.sin(rot) + 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 + 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,:,:]) -class PathCollection(_CollectionWithSizes): + def draw(self, renderer): + super(_CollectionWithAngles, self).draw(renderer) + + +class PathCollection(_CollectionWithSizes, _CollectionWithAngles): """ 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. %(Collection)s """ - - Collection.__init__(self, **kwargs) + super(PathCollection, self).__init__(**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 + def draw(self, renderer): + super(PathCollection, self).draw(renderer) class PolyCollection(_CollectionWithSizes): @docstring.dedent_interpd