Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Allow linear scaling for marker sizes in scatter #25259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ci/mypy-stubtest-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ matplotlib.collections.Collection.set_linewidths
matplotlib.collections.Collection.set_ls
matplotlib.collections.Collection.set_lw
matplotlib.collections.Collection.set_transOffset
matplotlib.collections._CollectionWithSizes.get_markersize
matplotlib.collections._CollectionWithSizes.get_s
matplotlib.collections._CollectionWithSizes.set_markersize
matplotlib.collections._CollectionWithSizes.set_s
matplotlib.lines.Line2D.get_aa
matplotlib.lines.Line2D.get_c
matplotlib.lines.Line2D.get_ds
Expand Down
5 changes: 5 additions & 0 deletions doc/users/next_whats_new/scatter_markersize.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
``markersize`` and ``markerscale`` added to scatter
---------------------------------------------------
``markersize`` is new alias for ``s`` in scatter that can be used to set the size of
the markers. The marker sizes can now also be set using a linear scale using the
``markerscale`` parameter.
21 changes: 18 additions & 3 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4476,7 +4476,7 @@ def invalid_shape_exception(csize, xsize):
@_docstring.interpd
def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
vmin=None, vmax=None, alpha=None, linewidths=None, *,
edgecolors=None, plotnonfinite=False, **kwargs):
edgecolors=None, plotnonfinite=False, markerscale=2, **kwargs):
"""
A scatter plot of *y* vs. *x* with varying marker size and/or color.

Expand Down Expand Up @@ -4568,6 +4568,12 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
or ``nan``). If ``True`` the points are drawn with the *bad*
colormap color (see `.Colormap.set_bad`).

Copy link
Member

@timhoffm timhoffm Feb 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not call this *scale, because it could be interpreted as "linear scaling factor" - and we do this e.g. at mutation_scale in https://matplotlib.org/devdocs/api/_as_gen/matplotlib.patches.FancyArrowPatch.html#matplotlib.patches.FancyArrowPatch. Suggestion: markerscaling or maybe just scaling - I have some sympathy to push this down to collectionWithSizes later. A scaling parameter would be meaningful there so that we have consistent naming and don't need to translate the markerscaling kwarg. Also, I believe scatter(..., scaling=...) is clear enough.

Also, 1 and 2 are not very telling as values. How about "linear"/"quadratic" or "linear"/"square" or "length"/"area"?

Edit: There seems to be a mismatch between the discussion and code whether to push the scaling down to CollectionWithSizes. I've written the above under the impression that pushing down should not (yet?) be done in this PR. - Rethinking, I claim that pushing down is very much to be preferred - not being able to reflect the high-level scaling concept in the underlying artists would be a major shortcoming. I'm +1 on implementing this immediately in CollectionWithSizes.

markerscale : 1 or 2, optional, default: 2
Scaling factor used to set the size as points or points**2.
Default value is set as 2 to set the size values as points**2.

.. versionadded:: 3.9

Returns
-------
`~matplotlib.collections.PathCollection`
Expand Down Expand Up @@ -4607,9 +4613,17 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
if x.size != y.size:
raise ValueError("x and y must be the same size")

if s is not None and 'markersize' in kwargs:
raise ValueError(
"Only one of `s` or `markersize` should be passed. "
"Please refer to the docs for more details about usage.")

if s is None:
s = (20 if mpl.rcParams['_internal.classic_mode'] else
mpl.rcParams['lines.markersize'] ** 2.0)
if 'markersize' not in kwargs:
s = (20 if mpl.rcParams['_internal.classic_mode'] else
mpl.rcParams['lines.markersize'] ** 2.0)
else:
s = kwargs.pop('markersize')
s = np.ma.ravel(s)
if (len(s) not in (1, x.size) or
(not np.issubdtype(s.dtype, np.floating) and
Expand Down Expand Up @@ -4696,6 +4710,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,

collection = mcoll.PathCollection(
(path,), scales,
markerscale=markerscale,
facecolors=colors,
edgecolors=edgecolors,
linewidths=linewidths,
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/axes/_axes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ class Axes(_AxesBase):
*,
edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = ...,
plotnonfinite: bool = ...,
markerscale: int = ...,
data=...,
**kwargs
) -> PathCollection: ...
Expand Down
47 changes: 40 additions & 7 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,9 @@ def update_from(self, other):
self.stale = True


@_api.define_aliases({
"sizes": ["s", "markersize"]
})
class _CollectionWithSizes(Collection):
"""
Base class for collections that have an array of sizes.
Expand All @@ -938,16 +941,35 @@ class _CollectionWithSizes(Collection):

def get_sizes(self):
"""
Return the sizes ('areas') of the elements in the collection.
Return the sizes of the elements in the collection.

The sizes of the elements in the collection will be returned in
the last set scale of the markers, i.e. if the markerscale was
set to 1 then the sizes are set in points and if markerscale was
set to 2 then the sizes returned are such that the marker size is
in points**2.

Returns
-------
array
The 'area' of each element.
The 'size' of each element.
"""
return self._sizes

def set_sizes(self, sizes, dpi=72.0):
def get_markerscale(self):
"""
Return the scale used for marker sizing.

.. versionadded:: 3.9

Returns
-------
int
The scale used to set the marker sizes.
"""
return self._markerscale

def set_sizes(self, sizes, dpi=72.0, markerscale=2):
"""
Set the sizes of each member of the collection.

Expand All @@ -958,22 +980,28 @@ def set_sizes(self, sizes, dpi=72.0):
value is the 'area' of the element.
dpi : float, default: 72
The dpi of the canvas.
markerscale : 1 or 2, default: 2
Scaling factor used to set the size as points (1) or points**2 (2).

.. versionadded:: 3.9
"""
self._markerscale = markerscale
if sizes is None:
self._sizes = np.array([])
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
s = np.sqrt(self._sizes) if self._markerscale == 2 else self._sizes
scale = s * dpi / 72.0 * self._factor
self._transforms[:, 0, 0] = scale
self._transforms[:, 1, 1] = scale
self._transforms[:, 2, 2] = 1.0
self.stale = True

@artist.allow_rasterization
def draw(self, renderer):
self.set_sizes(self._sizes, self.figure.dpi)
self.set_sizes(self._sizes, self.figure.dpi, self._markerscale)
super().draw(renderer)


Expand All @@ -982,7 +1010,7 @@ class PathCollection(_CollectionWithSizes):
A collection of `~.path.Path`\s, as created by e.g. `~.Axes.scatter`.
"""

def __init__(self, paths, sizes=None, **kwargs):
def __init__(self, paths, sizes=None, markerscale=2, **kwargs):
"""
Parameters
----------
Expand All @@ -992,13 +1020,18 @@ def __init__(self, paths, sizes=None, **kwargs):
The factor by which to scale each drawn `~.path.Path`. One unit
squared in the Path's data space is scaled to be ``sizes**2``
points when rendered.
markerscale : 1 or 2, default: 2
Scaling factor used to set the size as points (1) or points**2 (2).

.. versionadded:: 3.9

**kwargs
Forwarded to `.Collection`.
"""

super().__init__(**kwargs)
self.set_paths(paths)
self.set_sizes(sizes)
self.set_sizes(sizes, markerscale=markerscale)
self.stale = True

def set_paths(self, paths):
Expand Down
11 changes: 9 additions & 2 deletions lib/matplotlib/collections.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,18 @@ class Collection(artist.Artist, cm.ScalarMappable):

class _CollectionWithSizes(Collection):
def get_sizes(self) -> np.ndarray: ...
def set_sizes(self, sizes: ArrayLike | None, dpi: float = ...) -> None: ...
def set_sizes(
self, sizes: ArrayLike | None, dpi: float = ..., markerscale: int = ...
) -> None: ...
def get_markerscale(self) -> int: ...

class PathCollection(_CollectionWithSizes):
def __init__(
self, paths: Sequence[Path], sizes: ArrayLike | None = ..., **kwargs
self,
paths: Sequence[Path],
sizes: ArrayLike | None = ...,
markerscale: int = ...,
**kwargs
) -> None: ...
def set_paths(self, paths: Sequence[Path]) -> None: ...
def get_paths(self) -> Sequence[Path]: ...
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3587,6 +3587,7 @@ def scatter(
*,
edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = None,
plotnonfinite: bool = False,
markerscale: int = 2,
data=None,
**kwargs,
) -> PathCollection:
Expand All @@ -3604,6 +3605,7 @@ def scatter(
linewidths=linewidths,
edgecolors=edgecolors,
plotnonfinite=plotnonfinite,
markerscale=markerscale,
**({"data": data} if data is not None else {}),
**kwargs,
)
Expand Down
22 changes: 22 additions & 0 deletions lib/matplotlib/tests/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,3 +1216,25 @@ def test_striped_lines(fig_test, fig_ref, gapcolor):
for x, gcol, ls in zip(x, itertools.cycle(gapcolor),
itertools.cycle(linestyles)):
ax_ref.axvline(x, 0, 1, linestyle=ls, gapcolor=gcol, alpha=0.5)


@check_figures_equal(extensions=["png"])
def test_markerscale(fig_test, fig_ref):
ax_test = fig_test.add_subplot()
ax_ref = fig_ref.add_subplot()

x = np.arange(1, 6)**2

ax_test.scatter(x, x, s=x, markerscale=1)
ax_ref.scatter(x, x, s=x**2, markerscale=2)


@check_figures_equal(extensions=["png"])
def test_markersize_alias(fig_test, fig_ref):
ax_test = fig_test.add_subplot()
ax_ref = fig_ref.add_subplot()

x = np.arange(1, 6)**2

ax_test.scatter(x, x, s=x)
ax_ref.scatter(x, x, markersize=x)
4 changes: 2 additions & 2 deletions lib/mpl_toolkits/mplot3d/art3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,8 +688,8 @@ def set_3d_properties(self, zs, zdir):
self._vzs = None
self.stale = True

def set_sizes(self, sizes, dpi=72.0):
super().set_sizes(sizes, dpi)
def set_sizes(self, sizes, dpi=72.0, markerscale=2):
super().set_sizes(sizes, dpi=dpi, markerscale=markerscale)
if not self._in_draw:
self._sizes3d = sizes

Expand Down