Skip to content

Commit ca8db73

Browse files
committed
Introduce ArtistList for FigureBase
Following #18216 for Axes artists, combine all figure artists except axes and subfigures into a single list and deprecate modifying the lists directly.
1 parent ec53e36 commit ca8db73

16 files changed

Lines changed: 402 additions & 181 deletions

File tree

ci/mypy-stubtest-allowlist.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,6 @@ matplotlib\.animation\.EventSourceProtocol
5656
# Avoid a regression in NewType handling for stubtest
5757
# https://github.com/python/mypy/issues/19877
5858
matplotlib\.ft2font\.GlyphIndexType\.__init__
59+
60+
# 3.12 deprecation
61+
matplotlib\.axes\._base\._AxesBase\.ArtistList

doc/api/artist_api.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,14 @@ Functions
200200
getp
201201
setp
202202
kwdoc
203+
204+
Helper classes
205+
==============
206+
207+
.. autosummary::
208+
:template: autosummary.rst
209+
:toctree: _as_gen
210+
:nosignatures:
211+
203212
ArtistInspector
213+
ArtistList

doc/api/axes_api.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,5 +632,3 @@ Other
632632
Axes.get_figure
633633
Axes.figure
634634
Axes.remove
635-
636-
.. autoclass:: matplotlib.axes.Axes.ArtistList
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Direct modification of ``(Sub)Figure`` artist lists
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
Previously it was possible to modify the ``artists``, ``images``, ``lines``,
4+
``legends``, ``patches`` and ``texts`` attributes of `.Figure` and `.SubFigure`
5+
instances using standard `list` functionality. This is now deprecated.
6+
Instead use `~.Figure.add_artist` to add an artist to the figure, or use the
7+
artist's `remove` method to remove it.
8+
9+
10+
The ``Axes.AxesList`` attribute
11+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12+
... is deprecated. Use `.artist.ArtistList` instead.

galleries/tutorials/artists.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -314,24 +314,26 @@ class in the Matplotlib API, and the one you will be working with most
314314
#
315315
#
316316
# The figure also has its own ``images``, ``lines``, ``patches`` and ``text``
317-
# attributes, which you can use to add primitives directly. When doing so, the
318-
# default coordinate system for the ``Figure`` will simply be in pixels (which
319-
# is not usually what you want). If you instead use Figure-level methods to add
320-
# Artists (e.g., using `.Figure.text` to add text), then the default coordinate
321-
# system will be "figure coordinates" where (0, 0) is the bottom-left of the
322-
# figure and (1, 1) is the top-right of the figure.
323-
#
324-
# As with all ``Artist``\s, you can control this coordinate system by setting
325-
# the transform property. You can explicitly use "figure coordinates" by
326-
# setting the ``Artist`` transform to :attr:`!fig.transFigure`:
317+
# attributes, which you can use to access any primitives that are its direct
318+
# children. Adding images and text is usually achieved with the
319+
# `~.Figure.figimage` and `~.Figure.text` methods. Other artists may be added
320+
# with the `~.Figure.add_artist` method.
321+
#
322+
# As with all ``Artist``\s, you can control the coordinate system by setting
323+
# the transform property (see :ref:`transforms_tutorial`). When using
324+
# `~.Figure.figimage`, the default coordinate system is simply pixels. When
325+
# using `~.Figure.text` or `~.Figure.add_artist`, the default coordinate system
326+
# will be "figure coordinates" where (0, 0) is the bottom-left of the figure
327+
# and (1, 1) is the top-right of the figure.
327328

328329
import matplotlib.lines as lines
329330

330331
fig = plt.figure()
331332

332-
l1 = lines.Line2D([0, 1], [0, 1], transform=fig.transFigure, figure=fig)
333-
l2 = lines.Line2D([0, 1], [1, 0], transform=fig.transFigure, figure=fig)
334-
fig.lines.extend([l1, l2])
333+
line1 = lines.Line2D([0, 1], [0, 1])
334+
line2 = lines.Line2D([0, 1], [1, 0])
335+
for line in line1, line2:
336+
fig.add_artist(line)
335337

336338
plt.show()
337339

@@ -342,16 +344,18 @@ class in the Matplotlib API, and the one you will be working with most
342344
# Figure attribute Description
343345
# ================ ============================================================
344346
# axes A list of `~.axes.Axes` instances
347+
# subfigures A list of `.SubFigure` instances
345348
# patch The `.Rectangle` background
346-
# images A list of `.FigureImage` patches -
349+
# images An `~.artist.ArtistList` of `.FigureImage` patches -
347350
# useful for raw pixel display
348-
# legends A list of Figure `.Legend` instances
351+
# legends An `~.artist.ArtistList` of Figure `.Legend` instances
349352
# (different from ``Axes.get_legend()``)
350-
# lines A list of Figure `.Line2D` instances
353+
# lines An `~.artist.ArtistList` of Figure `.Line2D` instances
351354
# (rarely used, see ``Axes.lines``)
352-
# patches A list of Figure `.Patch`\s
355+
# patches An `~.artist.ArtistList` of Figure `.Patch`\s
353356
# (rarely used, see ``Axes.patches``)
354-
# texts A list Figure `.Text` instances
357+
# texts An `~.artist.ArtistList` of Figure `.Text` instances
358+
# artists An `~.artist.ArtistList` of all other `.Artist` instances
355359
# ================ ============================================================
356360
#
357361
# .. _axes-container:
@@ -562,13 +566,13 @@ class in the Matplotlib API, and the one you will be working with most
562566
# ============== =========================================
563567
# Axes attribute Description
564568
# ============== =========================================
565-
# artists An `.ArtistList` of `.Artist` instances
569+
# artists An `~.artist.ArtistList` of `.Artist` instances
566570
# patch `.Rectangle` instance for Axes background
567-
# collections An `.ArtistList` of `.Collection` instances
568-
# images An `.ArtistList` of `.AxesImage`
569-
# lines An `.ArtistList` of `.Line2D` instances
570-
# patches An `.ArtistList` of `.Patch` instances
571-
# texts An `.ArtistList` of `.Text` instances
571+
# collections An `~.artist.ArtistList` of `.Collection` instances
572+
# images An `~.artist.ArtistList` of `.AxesImage`
573+
# lines An `~.artist.ArtistList` of `.Line2D` instances
574+
# patches An `~.artist.ArtistList` of `.Patch` instances
575+
# texts An `~.artist.ArtistList` of `.Text` instances
572576
# xaxis A `matplotlib.axis.XAxis` instance
573577
# yaxis A `matplotlib.axis.YAxis` instance
574578
# ============== =========================================

lib/matplotlib/_api/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,12 @@ def warn_external(message, category=None):
470470
"""
471471
# Go to Python's `site-packages` or `lib` from an editable install.
472472
basedir = pathlib.Path(__file__).parents[2]
473-
skip_file_prefixes = (str(basedir / 'matplotlib'),
474-
str(basedir / 'mpl_toolkits'))
473+
skip_file_prefixes = (
474+
str(basedir / 'matplotlib'),
475+
str(basedir / 'mpl_toolkits'),
476+
# If we subclass a collections.abc class, the user may call an abc method that
477+
# calls our method. For example if we warn within insert on a MutableSequence,
478+
# and the user calls append or extend.
479+
'<frozen _collections_abc>')
475480

476481
warnings.warn(message, category, skip_file_prefixes=skip_file_prefixes)

lib/matplotlib/artist.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections import namedtuple
2+
from collections.abc import Sequence
23
import contextlib
34
from functools import cache, reduce, wraps
45
import inspect
@@ -1789,6 +1790,74 @@ def pprint_getters(self):
17891790
return lines
17901791

17911792

1793+
class ArtistList(Sequence):
1794+
"""
1795+
A sublist of Axes or Figure children based on their type.
1796+
1797+
The Axes' type-specific children sublists were made immutable in Matplotlib
1798+
3.7. In the future these artist lists may be replaced by tuples. Use
1799+
as if this is a tuple already.
1800+
"""
1801+
def __init__(self, parent, prop_name, valid_types=None, invalid_types=None):
1802+
"""
1803+
Parameters
1804+
----------
1805+
parent : `~matplotlib.axes.Axes` or `~matplotlib.figure.FigureBase`
1806+
The Axes or (Sub)Figure from which this sublist will pull the children
1807+
Artists.
1808+
prop_name : str
1809+
The property name used to access this sublist from the parent.
1810+
valid_types : list of type, optional
1811+
A list of types that determine which children will be returned
1812+
by this sublist. If specified, then the Artists in the sublist
1813+
must be instances of any of these types. If unspecified, then
1814+
any type of Artist is valid (unless limited by
1815+
*invalid_types*.)
1816+
invalid_types : tuple, optional
1817+
A list of types that determine which children will *not* be
1818+
returned by this sublist. If specified, then Artists in the
1819+
sublist will never be an instance of these types. Otherwise, no
1820+
types will be excluded.
1821+
"""
1822+
self._parent = parent
1823+
self._prop_name = prop_name
1824+
self._type_check = lambda artist: (
1825+
(not valid_types or isinstance(artist, valid_types)) and
1826+
(not invalid_types or not isinstance(artist, invalid_types))
1827+
)
1828+
1829+
def __repr__(self):
1830+
parent_type = self._parent.__class__.__name__
1831+
return f'<{parent_type}.ArtistList of {len(self)} {self._prop_name}>'
1832+
1833+
def __len__(self):
1834+
return sum(self._type_check(artist) for artist in self._parent._children)
1835+
1836+
def __iter__(self):
1837+
for artist in list(self._parent._children):
1838+
if self._type_check(artist):
1839+
yield artist
1840+
1841+
def __getitem__(self, key):
1842+
return [artist
1843+
for artist in self._parent._children
1844+
if self._type_check(artist)][key]
1845+
1846+
def __add__(self, other):
1847+
if isinstance(other, (list, ArtistList)):
1848+
return [*self, *other]
1849+
if isinstance(other, (tuple, ArtistList)):
1850+
return (*self, *other)
1851+
return NotImplemented
1852+
1853+
def __radd__(self, other):
1854+
if isinstance(other, list):
1855+
return other + list(self)
1856+
if isinstance(other, tuple):
1857+
return other + tuple(self)
1858+
return NotImplemented
1859+
1860+
17921861
def getp(obj, property=None):
17931862
"""
17941863
Return the value of an `.Artist`'s *property*, or print all of them.

lib/matplotlib/artist.pyi

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ from .transforms import (
1414

1515
import numpy as np
1616

17-
from collections.abc import Callable, Iterable
17+
from collections.abc import Callable, Iterable, Iterator, Sequence
1818
from typing import Any, Literal, NamedTuple, TextIO, overload, TypeVar
1919
from numpy.typing import ArrayLike
2020

@@ -189,6 +189,36 @@ class ArtistInspector:
189189
def properties(self) -> dict[str, Any]: ...
190190
def pprint_getters(self) -> list[str]: ...
191191

192+
193+
class ArtistList(Sequence[_T_Artist]):
194+
def __init__(
195+
self,
196+
parent: _AxesBase | Figure | SubFigure,
197+
prop_name: str,
198+
valid_types: type | Iterable[type] | None = ...,
199+
invalid_types: type | Iterable[type] | None = ...,
200+
) -> None: ...
201+
def __len__(self) -> int: ...
202+
def __iter__(self) -> Iterator[_T_Artist]: ...
203+
@overload
204+
def __getitem__(self, key: int) -> _T_Artist: ...
205+
@overload
206+
def __getitem__(self, key: slice) -> list[_T_Artist]: ...
207+
208+
@overload
209+
def __add__(self, other: ArtistList[_T_Artist]) -> list[_T_Artist]: ...
210+
@overload
211+
def __add__(self, other: list[Any]) -> list[Any]: ...
212+
@overload
213+
def __add__(self, other: tuple[Any]) -> tuple[Any]: ...
214+
215+
@overload
216+
def __radd__(self, other: ArtistList[_T_Artist]) -> list[_T_Artist]: ...
217+
@overload
218+
def __radd__(self, other: list[Any]) -> list[Any]: ...
219+
@overload
220+
def __radd__(self, other: tuple[Any]) -> tuple[Any]: ...
221+
192222
def getp(obj: Artist, property: str | None = ...) -> Any: ...
193223

194224
get = getp

lib/matplotlib/axes/_base.py

Lines changed: 12 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import Iterable, Sequence
1+
from collections.abc import Iterable
22
from contextlib import ExitStack
33
import functools
44
import inspect
@@ -1481,105 +1481,41 @@ def cla(self):
14811481
else:
14821482
self.clear()
14831483

1484-
class ArtistList(Sequence):
1485-
"""
1486-
A sublist of Axes children based on their type.
1487-
1488-
The type-specific children sublists were made immutable in Matplotlib
1489-
3.7. In the future these artist lists may be replaced by tuples. Use
1490-
as if this is a tuple already.
1491-
"""
1492-
def __init__(self, axes, prop_name,
1493-
valid_types=None, invalid_types=None):
1494-
"""
1495-
Parameters
1496-
----------
1497-
axes : `~matplotlib.axes.Axes`
1498-
The Axes from which this sublist will pull the children
1499-
Artists.
1500-
prop_name : str
1501-
The property name used to access this sublist from the Axes;
1502-
used to generate deprecation warnings.
1503-
valid_types : list of type, optional
1504-
A list of types that determine which children will be returned
1505-
by this sublist. If specified, then the Artists in the sublist
1506-
must be instances of any of these types. If unspecified, then
1507-
any type of Artist is valid (unless limited by
1508-
*invalid_types*.)
1509-
invalid_types : tuple, optional
1510-
A list of types that determine which children will *not* be
1511-
returned by this sublist. If specified, then Artists in the
1512-
sublist will never be an instance of these types. Otherwise, no
1513-
types will be excluded.
1514-
"""
1515-
self._axes = axes
1516-
self._prop_name = prop_name
1517-
self._type_check = lambda artist: (
1518-
(not valid_types or isinstance(artist, valid_types)) and
1519-
(not invalid_types or not isinstance(artist, invalid_types))
1520-
)
1521-
1522-
def __repr__(self):
1523-
return f'<Axes.ArtistList of {len(self)} {self._prop_name}>'
1524-
1525-
def __len__(self):
1526-
return sum(self._type_check(artist)
1527-
for artist in self._axes._children)
1528-
1529-
def __iter__(self):
1530-
for artist in list(self._axes._children):
1531-
if self._type_check(artist):
1532-
yield artist
1533-
1534-
def __getitem__(self, key):
1535-
return [artist
1536-
for artist in self._axes._children
1537-
if self._type_check(artist)][key]
1538-
1539-
def __add__(self, other):
1540-
if isinstance(other, (list, _AxesBase.ArtistList)):
1541-
return [*self, *other]
1542-
if isinstance(other, (tuple, _AxesBase.ArtistList)):
1543-
return (*self, *other)
1544-
return NotImplemented
1545-
1546-
def __radd__(self, other):
1547-
if isinstance(other, list):
1548-
return other + list(self)
1549-
if isinstance(other, tuple):
1550-
return other + tuple(self)
1551-
return NotImplemented
1484+
@_api.deprecated('3.12', alternative='matplotlib.artist.ArtistList')
1485+
@property
1486+
def ArtistList(self):
1487+
return martist.ArtistList
15521488

15531489
@property
15541490
def artists(self):
1555-
return self.ArtistList(self, 'artists', invalid_types=(
1491+
return martist.ArtistList(self, 'artists', invalid_types=(
15561492
mcoll.Collection, mimage.AxesImage, mlines.Line2D, mpatches.Patch,
15571493
mtable.Table, mtext.Text))
15581494

15591495
@property
15601496
def collections(self):
1561-
return self.ArtistList(self, 'collections',
1497+
return martist.ArtistList(self, 'collections',
15621498
valid_types=mcoll.Collection)
15631499

15641500
@property
15651501
def images(self):
1566-
return self.ArtistList(self, 'images', valid_types=mimage.AxesImage)
1502+
return martist.ArtistList(self, 'images', valid_types=mimage.AxesImage)
15671503

15681504
@property
15691505
def lines(self):
1570-
return self.ArtistList(self, 'lines', valid_types=mlines.Line2D)
1506+
return martist.ArtistList(self, 'lines', valid_types=mlines.Line2D)
15711507

15721508
@property
15731509
def patches(self):
1574-
return self.ArtistList(self, 'patches', valid_types=mpatches.Patch)
1510+
return martist.ArtistList(self, 'patches', valid_types=mpatches.Patch)
15751511

15761512
@property
15771513
def tables(self):
1578-
return self.ArtistList(self, 'tables', valid_types=mtable.Table)
1514+
return martist.ArtistList(self, 'tables', valid_types=mtable.Table)
15791515

15801516
@property
15811517
def texts(self):
1582-
return self.ArtistList(self, 'texts', valid_types=mtext.Text)
1518+
return martist.ArtistList(self, 'texts', valid_types=mtext.Text)
15831519

15841520
def get_facecolor(self):
15851521
"""Get the facecolor of the Axes."""

0 commit comments

Comments
 (0)